BIO.RE
User

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.

HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKApiResponseOf<GdprExportRequestDto>

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "PENDING",
    "createdAt": "2026-04-29T20:00:00.000Z"
  }
}
FieldTypeNotes
idstring (UUID)GDPRRequest.id — pass to status / download endpoints
statusenumAlways PENDING immediately after creation. Will progress through PROCESSINGCOMPLETED (or FAILED).
createdAtstring (ISO 8601)Server-side timestamp

Status lifecycle

StatusSet byMeaning
PENDINGThis endpointQueued; worker hasn't started yet
PROCESSINGWorkerArchive is being built
COMPLETEDWorkerArchive ready — /download will return a signed URL
FAILEDWorkerArchive build failed — surface the request to support
CANCELLED(admin / system)Operator-cancelled — never set by this endpoint

Errors

HTTPcode / i18nKeyReason
401(guard)Missing / invalid bearer token
409error.gdpr.export_already_pendingA previous EXPORT request is PENDING or PROCESSING
429(throttle)Rate limit exceeded (3 req/day)

Side effects

  1. Look for an existing GDPRRequest for this user with type = EXPORT AND status IN (PENDING, PROCESSING). If found, throw export_already_pending.
  2. Insert GDPRRequest { id: randomUUID(), userId, type: EXPORT, status: PENDING }.
  3. Audit log: [gdpr] Self-service export requested by user {userId}: {id}.
  4. The worker-service cron scans for PENDING exports, builds the archive, writes a DataExport row with fileUrl and expiresAt, and flips the GDPRRequest.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

POST
/api/v1/gdpr/export
AuthorizationBearer <token>

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

SourcePathLines
Controllerapps/api-core/src/modules/user/gdpr.controller.ts32–42 (requestExport)
DTO (response)apps/api-core/src/modules/user/dto/gdpr-response.dto.ts5–18 (GdprExportRequestDto)
Serviceapps/api-core/src/modules/user/user.service.ts561–581 (gdprRequestExport)
Worker pickupapps/worker-service/cron scans GDPRRequest with status = PENDING
Prisma modelspackages/prisma/prisma/schema.prismaGDPRRequest, DataExport, GDPRRequestType.EXPORT, GDPRRequestStatus

On this page