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_id mapping 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_assets rows.

What stays

  • analytics_anonymous_events - no person link by design, no PII.
  • consent_audit_log entries - 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' }
i
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

CodeStatusWhen
requires_secret_key403Called with a pk_* key.
not_found404getDeletionStatus called with an unknown jobId.
invalid_request400Empty or oversized distinctId / jobId.
i
Related: optOut() - non-destructive (reversible) right-to-object, exportPerson() - Art. 15 / 20, and the Privacy & GDPR guide.