BIO.RE
User

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.

GET /api/v1/gdpr/export/:id/download — 🔑 Bearer

Returns a time-limited signed URL for the completed export archive. Only succeeds when the request is COMPLETED; intermediate states return 404 export_not_ready. The URL is short-lived (expires at DataExport.expiresAt, falling back to +24h when no expiry is recorded).

The signed URL is single-shot in spirit. Don't email it, don't paste it in chat — anyone with the URL can download until it expires. Render into an <a href={downloadUrl} download> and trigger the click immediately, then discard from memory.

Returning a URL (rather than streaming the bytes) keeps the API server out of the data path — the file is served directly by object storage. The signing model is owned by the upload service; the URL works without further auth headers until expiresAt.

Request

Path parameters

ParamTypeValidationNotes
idstring (UUID)ParseUUIDPipeThe id returned by POST /gdpr/export
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKApiResponseOf<GdprExportDownloadDto>

{
  "success": true,
  "data": {
    "downloadUrl": "https://cdn.bio.re/exports/uuid/export.zip?sig=...",
    "expiresAt": "2026-04-30T20:04:32.000Z"
  }
}
FieldTypeNotes
downloadUrlstringTime-limited URL to the export archive — render directly into <a href download>
expiresAtstring (ISO 8601)When the URL stops working — pulled from DataExport.expiresAt, fallback now + 24h

Errors

HTTPcode / i18nKeyReason
400error.gdpr.not_exportThe GDPRRequest exists but is type DELETION, not EXPORT
401(guard)Missing / invalid bearer token
403error.gdpr.not_ownerRequest exists but userId does not match
404error.gdpr.request_not_foundNo GDPRRequest row with this id
404error.gdpr.export_not_readyStatus is PENDING / PROCESSING / FAILED / CANCELLED (not COMPLETED)
404error.gdpr.export_file_missingStatus is COMPLETED but no matching DataExport row with fileUrl was found

Side effects

  1. Lookup GDPRRequest; throw request_not_found if missing.
  2. Ownership check (request.userId === jwt.sub) → else not_owner.
  3. Type check (request.type === EXPORT) → else not_export.
  4. Status check (request.status === COMPLETED) → else export_not_ready.
  5. Find the user's most recent DataExport with status = COMPLETED AND fileUrl != null (orderBy: createdAt desc). If none → export_file_missing.
  6. Hand the file URL to the upload service, which produces a signed, time-limited URL suitable for direct browser download.
  7. Compute expiresAt = DataExport.expiresAt ?? (now + 24h).
  8. Return { downloadUrl, expiresAt }. No mutations.

Code samples

curl https://api.bio.re/api/v1/gdpr/export/a1b2c3d4-e5f6-7890-abcd-ef1234567890/download \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type GdprExportDownload = {
  downloadUrl: string;
  expiresAt: string;
};

async function getExportDownload(accessToken: string, id: string): Promise<GdprExportDownload> {
  const res = await fetch(`https://api.bio.re/api/v1/gdpr/export/${id}/download`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Download URL fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}

// Trigger the download immediately so the URL doesn't linger in memory
function triggerDownload(downloadUrl: string) {
  const a = document.createElement('a');
  a.href = downloadUrl;
  a.download = '';
  document.body.appendChild(a);
  a.click();
  a.remove();
}
import { useMutation } from '@tanstack/react-query';

export function useGetExportDownload() {
  // Mutation — not a query — because it produces a side-effect (triggers download)
  // and we never want to cache the signed URL.
  return useMutation({
    mutationFn: async (id: string) => {
      const res = await fetch(`/api/v1/gdpr/export/${id}/download`);
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Download URL fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as GdprExportDownload;
    },
  });
}

Try it

GET
/api/v1/gdpr/export/{id}/download
AuthorizationBearer <token>

In: header

Path Parameters

id*string

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://loading/api/v1/gdpr/export/string/download"
{
  "success": true,
  "data": {
    "downloadUrl": "https://cdn.bio.re/exports/uuid/export.zip?sig=...",
    "expiresAt": "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.ts57–68 (downloadExport)
DTO (response)apps/api-core/src/modules/user/dto/gdpr-response.dto.ts41–47 (GdprExportDownloadDto)
Serviceapps/api-core/src/modules/user/user.service.ts603–631 (gdprDownloadExport)
URL signingapps/api-core/src/modules/upload/upload.service.tsgetSignedDownloadUrl()
Prisma modelspackages/prisma/prisma/schema.prismaGDPRRequest, DataExport.fileUrl, DataExport.expiresAt

On this page