Request Data Export (GDPR)
Right-of-Access (Art. 15) — queue an export of the user's data. Returns a request id that the client polls until COMPLETED, then downloads via the signed-URL endpoint.
POST /api/v1/gdpr/export — 🔑 Bearer · Rate limit: 3 req / day
Creates a GDPRRequest row of type EXPORT in PENDING state. A worker-service cron picks the row up, builds the export archive, writes a DataExport row, and flips the request status to COMPLETED (or FAILED). Clients poll GET /gdpr/export/:id/status until completion, then call GET /gdpr/export/:id/download for the signed URL.
This is the modern export endpoint. The legacy alias POST /users/export still exists for backward compatibility but only checks PENDING (not PROCESSING) when guarding against duplicates and returns the simpler { requestId } shape. New clients should use this endpoint.
Only one export can be in flight per user — if a PENDING or PROCESSING GDPRRequest EXPORT already exists, the call is rejected with 409 export_already_pending. Wait for the previous one to complete (or hit the 3/day limit and try again tomorrow).
Request
No body, no params.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<GdprExportRequestDto>
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "PENDING",
"createdAt": "2026-04-29T20:00:00.000Z"
}
}| Field | Type | Notes |
|---|---|---|
id | string (UUID) | GDPRRequest.id — pass to status / download endpoints |
status | enum | Always PENDING immediately after creation. Will progress through PROCESSING → COMPLETED (or FAILED). |
createdAt | string (ISO 8601) | Server-side timestamp |
Status lifecycle
| Status | Set by | Meaning |
|---|---|---|
PENDING | This endpoint | Queued; worker hasn't started yet |
PROCESSING | Worker | Archive is being built |
COMPLETED | Worker | Archive ready — /download will return a signed URL |
FAILED | Worker | Archive build failed — surface the request to support |
CANCELLED | (admin / system) | Operator-cancelled — never set by this endpoint |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
409 | error.gdpr.export_already_pending | A previous EXPORT request is PENDING or PROCESSING |
429 | (throttle) | Rate limit exceeded (3 req/day) |
Side effects
- Look for an existing
GDPRRequestfor this user withtype = EXPORTANDstatus IN (PENDING, PROCESSING). If found, throwexport_already_pending. - Insert
GDPRRequest { id: randomUUID(), userId, type: EXPORT, status: PENDING }. - Audit log:
[gdpr] Self-service export requested by user {userId}: {id}. - The worker-service cron scans for
PENDINGexports, builds the archive, writes aDataExportrow withfileUrlandexpiresAt, and flips theGDPRRequest.status. This pickup is asynchronous — typical completion is on the order of minutes; clients should poll/gdpr/export/:id/status.
Code samples
curl -X POST https://api.bio.re/api/v1/gdpr/export \
-H "Authorization: Bearer $ACCESS_TOKEN"type GdprStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
type GdprExportRequest = {
id: string;
status: GdprStatus;
createdAt: string;
};
async function requestGdprExport(accessToken: string): Promise<GdprExportRequest> {
const res = await fetch('https://api.bio.re/api/v1/gdpr/export', {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Export request failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useRequestGdprExport() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => {
const res = await fetch('/api/v1/gdpr/export', { method: 'POST' });
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Export request failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as GdprExportRequest;
},
onSuccess: (data) => {
// Seed the status cache so the polling query starts with the freshly created row
qc.setQueryData(['gdpr', 'export', data.id, 'status'], {
id: data.id,
status: data.status,
createdAt: data.createdAt,
completedAt: null,
});
},
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/gdpr/export"{
"success": true,
"data": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"status": "PENDING",
"createdAt": "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 | 32–42 (requestExport) |
| DTO (response) | apps/api-core/src/modules/user/dto/gdpr-response.dto.ts | 5–18 (GdprExportRequestDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 561–581 (gdprRequestExport) |
| Worker pickup | apps/worker-service/ | cron scans GDPRRequest with status = PENDING |
| Prisma models | packages/prisma/prisma/schema.prisma | GDPRRequest, DataExport, GDPRRequestType.EXPORT, GDPRRequestStatus |
Reactivate Account
Bring a DEACTIVATED account back to ACTIVE. Two auth modes — bearer JWT (logged-in user) OR opaque token from email link (no JWT required). Cancels any pending deletion in the same transaction.
Get Export Status
Poll the status of a previously created GDPR export request. Returns the current state plus completedAt when finished.