Request Deletion (Legacy)
Older /users/delete endpoint — same effect as POST /gdpr/delete but additionally requires the current account password and returns only the scheduled date.
POST /api/v1/users/delete — 🔑 Bearer · Rate limit: 3 req / hour
Legacy alias of POST /gdpr/delete. Same effect (scheduled deletion + immediate deactivation + session revoke), but it additionally requires the current account password in the body and returns the leaner { scheduledAt } shape.
New clients should use POST /gdpr/delete. This legacy endpoint differs from the modern equivalent in three ways:
- Body is required —
{ password }must be sent and is bcrypt-checked againstUser.passwordHash. - Response is leaner —
{ scheduledAt }only, no request id or status field. - Throttle is looser — 3/hour vs 1/day on the modern endpoint.
To cancel a deletion requested through this endpoint, use DELETE /gdpr/delete — both endpoints write to the same GDPRRequest table, so the cancel endpoint sees both.
Request
Body — RequestDeletionDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
password | string | ✓ | MinLength(8) | Current account password — bcrypt-compared against User.passwordHash |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<DeletionScheduledDto>
{
"success": true,
"data": {
"scheduledAt": "2026-05-29T20:00:00.000Z"
}
}| Field | Type | Notes |
|---|---|---|
scheduledAt | string (ISO 8601) | When the worker will run the actual purge — now + gdpr.delete_grace_days (admin-managed, default 30 days) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | password shorter than 8 chars |
400 | error.user.password_required | Account has no passwordHash (OAuth-only — set a password first or use the modern endpoint which doesn't require one) |
400 | error.user.password_incorrect | Bcrypt password check failed |
401 | (guard) | Missing / invalid bearer token |
404 | error.user.not_found | Token decoded but user row missing |
409 | error.user.deletion_scheduled | A previous DELETION request is already PENDING |
429 | (throttle) | Rate limit exceeded (3 req/hour) |
Side effects
- Lookup
User; reject if missing or nopasswordHash. - Bcrypt-compare
passwordagainstUser.passwordHash. - Look for an existing
GDPRRequest { type: DELETION, status: PENDING }. If found →deletion_scheduled. - Compute
scheduledAt = now + gdpr.delete_grace_days * 86400 * 1000. - Inside one transaction (same as
/gdpr/delete):- Insert
GDPRRequest { id, userId, type: DELETION, status: PENDING, scheduledAt }. User.status = DEACTIVATED.- Revoke every active
Session.
- Insert
- Audit log:
[gdpr] Deletion scheduled for user {userId} at <iso>.
Migration to the modern endpoint
- await fetch('/api/v1/users/delete', {
- method: 'POST',
- body: JSON.stringify({ password }),
- });
+ // The modern endpoint does not require the password — re-confirm via UI flow if you want
+ await fetch('/api/v1/gdpr/delete', { method: 'POST' });If you need the password-confirmation UX (recommended for destructive operations), keep using this legacy endpoint or add an interstitial password re-prompt in your app before calling /gdpr/delete.
Code samples
curl -X POST https://api.bio.re/api/v1/users/delete \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"password": "current-account-password"}'async function requestDeletionLegacy(accessToken: string, password: string): Promise<string> {
const res = await fetch('https://api.bio.re/api/v1/users/delete', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Deletion request failed'), {
code: json?.error?.code,
});
}
return json.data.scheduledAt as string;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useRequestDeletionLegacy() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (password: string) => {
const res = await fetch('/api/v1/users/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Deletion request failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data.scheduledAt as string;
},
onSuccess: () => {
// Sessions revoked server-side — drop all caches, force re-auth flow
qc.clear();
},
});
}Try it
Authorization
bearer In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/users/delete" \ -H "Content-Type: application/json" \ -d '{ "password": "stringst" }'{
"success": true,
"data": {
"scheduledAt": "2019-08-24T14:15:22Z"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/user/user.controller.ts | 223–233 (requestDeletion) |
| DTO (request) | apps/api-core/src/modules/user/dto/index.ts | 16–18 (RequestDeletionDto) |
| DTO (response) | apps/api-core/src/modules/user/dto/user-client-response.dto.ts | 168–171 (DeletionScheduledDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 483–526 (requestDeletion) |
| Modern equivalent | apps/api-core/src/modules/user/gdpr.controller.ts | 72–82 (requestDeletion) |
| Prisma models | packages/prisma/prisma/schema.prisma | GDPRRequest, User.status, Session.revoked |
Cancel Pending Deletion
Stop a previously scheduled GDPR deletion before the grace period ends. Reactivates the account in the same transaction. Sessions stay revoked — re-login required.
Record Consent
Append-only log of user consent decisions (TOS, privacy, cookies, etc). Captures IP and user-agent for compliance audit.