vevee.analytics.deletePerson() · getDeletionStatus()POST /api/v1/delete-personGET /api/v1/deletion-statussk_live_
GDPR Art. 17 - right to erasure. Permanently deletes everything APL holds for a user across analytics and metering. Requires a secret key (sk_*) - backend-only.
Signatures
vevee.analytics.deletePerson(distinctId: string): Promise<{ jobId: string }>
vevee.analytics.getDeletionStatus(jobId: string): Promise<{
status: 'queued' | 'in_progress' | 'completed' | 'failed';
startedAt?: string;
completedAt?: string;
errorMessage?: string;
}>What gets deleted
When the worker drains the job, the following are removed for that person:
- All identified events (
analytics_events). - The person profile and every
distinct_idmapping pointing at it. - All metering data tied to the
end_user_id:events,counters,subscriptions,subscription_events,reservations,event_logs,credit_balances,credit_ledger,media_assetsrows.
What stays
analytics_anonymous_events- no person link by design, no PII.consent_audit_logentries - kept 5 years as legal evidence (consent_given, action and timestamp only, no profile data).- R2 object bytes for media - row removal makes them orphans; the existing R2 cleanup is what removes the bytes.
Flow
// Backend route reacting to a "delete my data" form submission.
const { jobId } = await vevee.analytics.deletePerson('user_12345');
// Acknowledge the request immediately - the worker drains asynchronously.
return Response.json({ ok: true, deletionJobId: jobId });Optional: surface progress to the user.
const status = await vevee.analytics.getDeletionStatus(jobId);
// → { status: 'in_progress', startedAt: '2026-05-23T08:00:00Z' }Mid-erasure capture is silently dropped. Between
deletePerson() and the worker finishing, the person row carries pending_deletion = 1, so further capture() calls for that id drop ahead of the worker - no new data is created mid-erasure.Completion target
7 days target, 30 days maximum (GDPR Art. 12). Drain cadence is governed by your external trigger of /api/cron/analytics-deletion-worker - the cron endpoint is authenticated via CRON_SECRET and is meant to be called by an external worker (a GitHub Actions cron, an external scheduler, or your own server). It is not wired in vercel.json.
End-to-end backend example
// app/api/privacy/delete/route.ts
import { aplClient } from '@/lib/vevee'; // createClient({ apiKey: 'sk_live_…' })
export async function POST(req: Request) {
const { userId } = await req.json();
// Also delete from YOUR OWN database here. APL's erasure only covers APL.
await db.user.deleteAccountData(userId);
const { jobId } = await aplClient.analytics.deletePerson(userId);
return Response.json({ ok: true, jobId });
}
// app/api/privacy/delete/status/route.ts
export async function GET(req: Request) {
const jobId = new URL(req.url).searchParams.get('jobId')!;
return Response.json(await aplClient.analytics.getDeletionStatus(jobId));
}Errors
| Code | Status | When |
|---|---|---|
requires_secret_key | 403 | Called with a pk_* key. |
not_found | 404 | getDeletionStatus called with an unknown jobId. |
invalid_request | 400 | Empty or oversized distinctId / jobId. |
Related: optOut() - non-destructive (reversible) right-to-object, exportPerson() - Art. 15 / 20, and the Privacy & GDPR guide.