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.
POST /api/v1/users/export โ ๐ Bearer ยท Rate limit: 3 req / hour
Legacy alias of POST /gdpr/export. Same purpose, slightly different behavior โ kept for backward compatibility.
New clients should use POST /gdpr/export. This legacy endpoint differs from it in two ways:
- Duplicate-check is looser โ it only blocks when an existing request is
PENDING. The modern endpoint blocks onPENDINGorPROCESSING. - Response is leaner โ only
{ requestId }is returned, nostatusorcreatedAt.
The throttle ceiling is also higher here (3 / hour vs 3 / day on the modern endpoint), which exists for historical reasons.
Request
No body, no params.
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | โ | JWT from POST /auth/login |
Response
200 OK โ ApiResponseOf<ExportRequestedDto>
{
"success": true,
"data": {
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}| Field | Type | Notes |
|---|---|---|
requestId | string (UUID) | GDPRRequest.id โ pass to GET /gdpr/export/:id/status and GET /gdpr/export/:id/download (the modern status / download endpoints accept ids created by either path) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | (guard) | Missing / invalid bearer token |
409 | error.user.export_in_progress | A previous EXPORT request is PENDING (does not trigger on PROCESSING โ that's the modern-endpoint behavior) |
429 | (throttle) | Rate limit exceeded (3 req/hour) |
Side effects
- Look for an existing
GDPRRequest { type: EXPORT, status: PENDING }for this user. If found โexport_in_progress. - Insert
GDPRRequest { id: randomUUID(), userId, type: EXPORT, status: PENDING }. - Audit log:
[gdpr] Export requested for user {userId}: {requestId}. - Worker pickup happens the same way as the modern endpoint โ there is one shared worker that processes all
PENDINGGDPRRequest EXPORTrows regardless of which controller created them.
Migration to the modern endpoint
- const res = await fetch('/api/v1/users/export', { method: 'POST' });
- const { requestId } = (await res.json()).data;
+ const res = await fetch('/api/v1/gdpr/export', { method: 'POST' });
+ const { id, status, createdAt } = (await res.json()).data;The status / download endpoints (GET /gdpr/export/:id/status and GET /gdpr/export/:id/download) work identically against either id โ there is no need to migrate already-issued ids.
Code samples
curl -X POST https://api.bio.re/api/v1/users/export \
-H "Authorization: Bearer $ACCESS_TOKEN"async function requestExportLegacy(accessToken: string): Promise<string> {
const res = await fetch('https://api.bio.re/api/v1/users/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.requestId;
}import { useMutation } from '@tanstack/react-query';
export function useRequestExportLegacy() {
return useMutation({
mutationFn: async () => {
const res = await fetch('/api/v1/users/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.requestId as string;
},
});
}Try it
Authorization
bearer In: header
Response Body
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/users/export"{
"success": true,
"data": {
"requestId": "d385ab22-0f51-4b97-9ecd-b8ff3fd4fcb6"
}
}{
"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 | 235โ244 (requestExport) |
| DTO (response) | apps/api-core/src/modules/user/dto/user-client-response.dto.ts | 175โ178 (ExportRequestedDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 531โ550 (requestDataExport) |
| Modern equivalent | apps/api-core/src/modules/user/gdpr.controller.ts | 32โ42 (requestExport) |
| Prisma model | packages/prisma/prisma/schema.prisma | GDPRRequest, GDPRRequestType.EXPORT |
Download Export
Once status is COMPLETED, fetch a time-limited signed URL for the export archive. The URL is short-lived โ render it into an `<a download>` and trigger immediately.
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.