Request Account Deletion (GDPR)
Right-to-Erasure (Art. 17) — schedule the account for deletion after a grace period. Account is deactivated immediately and all sessions revoked.
POST /api/v1/gdpr/delete — 🔑 Bearer · Rate limit: 1 req / day
Schedules the calling account for permanent deletion. Creates a GDPRRequest DELETION PENDING row, deactivates the account immediately (User.status = DEACTIVATED), and revokes every active session in the same transaction. The actual purge runs after a grace period (gdpr.delete_grace_days, admin-managed, default 30) — until then, the user can cancel via DELETE /gdpr/delete (or by clicking the reactivation link from the email flow).
Sessions are revoked immediately, including the one calling this endpoint. The user must re-authenticate to interact with the account again. Plan a redirect to a logged-out landing page on success.
For the legacy alias POST /users/delete, see Request Deletion (Legacy). The legacy endpoint additionally requires the current account password in the body — kept for backward compatibility but the modern endpoint here is preferred.
Request
No body, no params.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<GdprDeletionRequestDto>
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "PENDING",
"gracePeriodEnds": "2026-05-29T20:00:00.000Z"
}
}| Field | Type | Notes |
|---|---|---|
id | string (UUID) | GDPRRequest.id — pass to DELETE /gdpr/delete to cancel |
status | enum | Always PENDING immediately after creation. Will progress to PROCESSING → COMPLETED (worker-driven) or CANCELLED (user-driven via cancel endpoint). |
gracePeriodEnds | string (ISO 8601) | When the actual purge will run — now + gdpr.delete_grace_days (default 30 days) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
409 | error.gdpr.deletion_already_pending | A previous DELETION request is already PENDING |
429 | (throttle) | Rate limit exceeded (1 req/day) |
Side effects
- Look for an existing
GDPRRequest { type: DELETION, status: PENDING }for this user. If found →deletion_already_pending. - Compute
gracePeriodEnds = now + gdpr.delete_grace_days * 86400 * 1000. - Inside one transaction:
- Insert
GDPRRequest { id, userId, type: DELETION, status: PENDING, scheduledAt: gracePeriodEnds }. User.status = DEACTIVATED— account is hidden during the grace period.UPDATE Session SET revoked = true, revokedAt = now() WHERE userId = :userId AND revoked = false.
- Insert
- Audit log:
[gdpr] Self-service deletion requested by user {userId}, grace ends <iso>. - Worker pickup — after
gracePeriodEnds, the worker-service cron flipsstatustoPROCESSING, performs the purge (Art. 17 erasure across all owned data), and setsstatus = COMPLETED. The cancel endpoint becomes a no-op once status leavesPENDING.
Cancel flow
To stop a scheduled deletion before the grace period ends:
# Cancel + reactivate the account in one call
curl -X DELETE https://api.bio.re/api/v1/gdpr/delete \
-H "Authorization: Bearer $ACCESS_TOKEN"The cancel endpoint flips GDPRRequest.status = CANCELLED and User.status = ACTIVE atomically. Sessions stay revoked — the user must log in fresh after cancelling.
Code samples
curl -X POST https://api.bio.re/api/v1/gdpr/delete \
-H "Authorization: Bearer $ACCESS_TOKEN"type GdprDeletionRequest = {
id: string;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
gracePeriodEnds: string;
};
async function requestGdprDeletion(accessToken: string): Promise<GdprDeletionRequest> {
const res = await fetch('https://api.bio.re/api/v1/gdpr/delete', {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
});
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;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useRequestGdprDeletion() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => {
const res = await fetch('/api/v1/gdpr/delete', { method: 'POST' });
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 as GdprDeletionRequest;
},
onSuccess: () => {
// Sessions revoked server-side — drop all caches, force re-auth flow
qc.clear();
},
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/gdpr/delete"{
"success": true,
"data": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"status": "PENDING",
"gracePeriodEnds": "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/gdpr.controller.ts | 72–82 (requestDeletion) |
| DTO (response) | apps/api-core/src/modules/user/dto/gdpr-response.dto.ts | 51–63 (GdprDeletionRequestDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 638–678 (gdprRequestDeletion) |
| Worker pickup | apps/worker-service/ | cron scans GDPRRequest with type=DELETION, status=PENDING, scheduledAt <= now() |
| Prisma models | packages/prisma/prisma/schema.prisma | GDPRRequest, User.status, Session.revoked |
Request Export (Legacy)
Older /users/export alias. Same intent as POST /gdpr/export but with a simpler response shape and a slightly looser duplicate-check. Prefer the GDPR endpoint for new clients.
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.