GDPR Art. 15 (access) + Art. 20 (portability). Produces a signed download URL for a JSON dump of everything APL holds for a user. Requires a secret key (sk_*) - backend-only.
Signature
vevee.analytics.exportPerson(distinctId: string): Promise<{
downloadUrl: string;
expiresAt: string; // ISO 8601 - URL is valid for 24h
format: 'json';
}>Flow
// In your backend, in response to an Art. 15 request:
const { downloadUrl, expiresAt } = await vevee.analytics.exportPerson('user_12345');
// Email / show the URL to the data subject. It is a self-contained,
// time-bound bearer URL - no further auth needed to download.
await sendEmail({
to: user.email,
subject: 'Your data export',
body: `Download: ${downloadUrl} (expires ${expiresAt})`,
});What’s in the export
{
"format": "json",
"exportedAt": "2026-05-23T10:00:00Z",
"appId": "app_abc",
"mode": "live",
"distinctId": "user_12345",
"person": { ...analytics_persons row... },
"events": {
"analytics": [ ...analytics_events... ],
"metering": [ ...events... ]
},
"consentLog": [ ...consent_audit_log entries... ]
}Anonymous events are deliberately not included- they carry no person link by design and aren’t “their data” in any portable sense.
How the URL works
The download URL points at GET /api/v1/export-download?token=.... The token is an HMAC-SHA-256-signed payload - { appId, mode, distinctId, expiresAt } - signed with the ANALYTICS_EXPORT_SECRET env on the APL server. No API key is needed to download: the token is the authorisation.
The export is computed on demand when the URL is opened; no copy is staged in object storage. After expiresAt, the token is invalid; mint a new URL by calling exportPerson() again.
ANALYTICS_EXPORT_SECRET (≥16 chars) on the APL deployment for this endpoint to work. If running APL self-hosted, set it in your environment alongside the other secrets.Combine with your own data
The exporter only knows what APL holds. For a complete Art. 15 / 20 response you should also include the user’s data from your own database and any other processor in scope.
// app/api/privacy/export/route.ts
import { aplClient } from '@/lib/vevee';
export async function POST(req: Request) {
const { userId } = await req.json();
// Your own data:
const own = await db.user.exportAllData(userId);
// APL's data:
const { downloadUrl, expiresAt } = await aplClient.analytics.exportPerson(userId);
return Response.json({
ownDataInline: own,
aplDownloadUrl: downloadUrl,
aplDownloadExpiresAt: expiresAt,
});
}Errors
| Code | Status | When |
|---|---|---|
requires_secret_key | 403 | POST /export-person called with a pk_* key. |
invalid_or_expired_token | 401 | GET /export-download with a token that is past expiresAt or tampered with. |
invalid_request | 400 | Empty or oversized distinctId. |