Section 4: Broadcast, Don't Command - API Design (with Pushback)
Executive Summary: The Privacy Request Orchestrator has two audiences who should never see each other's API. Users get four endpoints that treat a privacy request as a long-running operation: submit it, watch its phase, cancel it during the grace period, download the export. The 1,200 registered data holders get a different contract: a registration manifest, a broadcast event that carries no personal data, a scoped identifier lookup, and an idempotent PUT acknowledgement bound to their mTLS identity. This section walks both faces with Lena's requests and defends five choices under pushback: why there is no DELETE webhook, why 202 and not 200, why users can't see per-service progress, why acks are PUT, and why the event leaves out her email.
Next prompt: "Show me the API." The tempting answer is one line, DELETE /users/{id} on every service. You draw a line down the middle of the board instead.
"There are two APIs here, and they have opposite jobs. The left side is a promise to a person: we've got your request, here's your deadline, here's how to change your mind. The right side is a contract with 1,200 services: here's an obligation, tell us what you did, and prove it was you. The orchestrator never deletes anything itself; it hands out obligations and records the answers."
The User Face: A Long-Running Operation
Lena has re-authenticated with 2FA. A logged-in session alone isn't enough here: a stolen session must never become an irreversible deletion. She clicks "Delete my account":
POST /v1/privacy-requests
Cookie: sid=... (session re-authenticated with 2FA moments ago)
Idempotency-Key: 6f1e2a9c-4b7d-4e0a-9d3f-1c2b7a8e5d40
Content-Type: application/json
{ "type": "ERASURE", "scope": "ALL" }
202 Accepted
Location: /v1/privacy-requests/dsr_01HQZ…DEL
{
"request_id": "dsr_01HQZ…DEL",
"state": "RECEIVED",
"legal_deadline": "2026-04-09T09:12:00Z",
"cancel_until": "2026-03-24T09:12:00Z"
}
Both timestamps are computed once at receipt and stored. legal_deadline is the earlier of receipt plus 30 days and the same date next month (EU time-limit counting, which bites for late-January requests); cancel_until is receipt plus the 14-day grace period. Returning them in the first response makes the deadline part of the contract. The Idempotency-Key covers the double-click and the mobile retry: the same key returns the same request_id, so two clicks never create two grace windows. As a second guard, the server allows one open erasure per subject.
Polling returns the phase only:
GET /v1/privacy-requests/dsr_01HQZ…DEL
200 OK
{ "request_id": "dsr_01HQZ…DEL", "type": "ERASURE", "state": "GRACE",
"legal_deadline": "2026-04-09T09:12:00Z", "cancel_until": "2026-03-24T09:12:00Z" }
Users see five phases: RECEIVED, GRACE, IN_PROGRESS, COMPLETED, CANCELLED. The finer internal states in Section 7 (fanning out, awaiting acks, verifying) all show up as IN_PROGRESS. If the deadline is extended, the response also carries extended_deadline and the reason, because the user must be told. The way back is POST /v1/privacy-requests/{id}:cancel, which works only in GRACE. After day 14 it returns 409 Conflict: fan-out has begun and deletion can no longer be undone.
Her export from earlier in the month uses the same resource with { "type": "ACCESS", "scope": ["mail", "maps", "photos"] }. When it's ready, she sees the chunk list, and each download needs a fresh re-authentication:
GET /v1/privacy-requests/dsr_01HQX…EXP/export
200 OK
{ "total_bytes": 25769803776, "available_until": "2026-03-12T10:40:00Z",
"chunks": [ { "part": 1, "bytes": 2147483648 }, …12 chunks of 2 GB, 24 GB in total… ] }
POST /v1/privacy-requests/dsr_01HQX…EXP/export/chunks/1:download (fresh re-auth)
200 OK
{ "url": "https://export.example.com/…?sig=…", "expires_at": "2026-03-05T10:55:00Z" }
The archive stays staged for 7 days, but a signed URL is a bearer token, so each one covers one chunk and lives 15 minutes. A forwarded link dies quickly.
Intake averages about one request per second, sized for 30/s bursts, p99 under 500 ms. The 202 is sent only after the multi-region commit, so an accepted request can't be lost (RPO 0) and a failed one fails loudly for a retry.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson