Delete a user's data
When one of your users exercises their right to erasure — GDPR Article 17, CCPA deletion, or simply "please delete my account" — you forward that request to Crossdeck with a single forget() call. Crossdeck immediately marks the customer record as forgotten, records a hard 30-day deletion deadline, writes a tamper-evident audit entry, and queues the deletion of the customer's stored data — all in one atomic transaction, so none of those four things can happen without the others. This page describes exactly what is deleted, what is retained and why, who is allowed to request erasure on whose behalf, and what you can show an auditor afterwards.
TL;DR
- One call from any SDK or the REST API.
POST /v1/identity/forgetwith any identity hint you hold —customerId,userId, oranonymousId. Client SDKs also wipe all on-device state. - The request is recorded atomically. The customer record is marked
forgottenAt, a deletion job is queued, and an append-only audit entry is written — in a single transaction. There is no path where an erasure is recorded without its audit row, or audited without being scheduled. - 30-day deadline, fixed at request time. Each request carries
forgetDeadlineAt = requestedAt + 30 days, matching GDPR's "within one month". The deadline is stored on both the customer record and the deletion-queue row, so an overdue request is mechanically detectable — not a matter of anyone remembering. - Erasing an identified user requires proof. Either a verified ID token from your auth provider whose subject equals the
userId, or a call from your backend with your secret key (cd_sk_*). A publishable key alone cannot erase an identified user — that would let anyone who knows a user ID destroy that user's data. - Financial transaction records are retained. Purchase, subscription, and refund records that originate from your payment rails (Stripe, Apple, Google) survive erasure. Legal and accounting retention obligations for financial transactions supersede erasure rights — this is stated in full below, not buried.
- The erasure itself is auditable forever. The audit entry lands in a hash-chained, append-only journal. You can later prove the erasure happened, when, on whose authority, and that the record has not been tampered with.
- Idempotent. Calling
forget()twice for the same person is safe — the audit trail collapses to a single erasure decision, and a request that matches no known customer still returns200.
What erasure covers
A forget() request targets one customer — the canonical Crossdeck customer record (cdcust_…) that your identity hint resolves to. On acceptance, Crossdeck does three things immediately and atomically:
- Marks the customer record. The customer document is stamped
forgottenAt(the request time) andforgetDeadlineAt(30 days later). - Writes the audit entry. An
erasure_executedentry is appended to the identity journal — the same append-only, hash-chained log that records every identity decision Crossdeck makes. See The audit trail. - Queues the deletion. A durable deletion-queue row is written, carrying the resolved customer ID, the original identity hints, the request time, and the deadline. The stored data deletion — event rows, identity audit rows, entitlement rows — is processed from this queue asynchronously, because deleting a customer's history can touch thousands of rows and a synchronous HTTP request is not the place to coordinate that.
Client-side, the Web and iOS SDKs additionally wipe everything they hold on the device the moment forget() is called: the stored identity, entitlement cache, the offline event queue, super-properties, and persistent stores. After the call, the SDK is in the same state as a fresh install — the next session mints a brand-new anonymous ID and starts a new entry in the identity graph.
Independently of erasure requests, Crossdeck's analytics warehouse enforces a global retention window: event data older than 24 months is dropped wholesale, by monthly partition, on a scheduled monthly sweep. This is a storage-wide ceiling on how long any event data can exist — it runs regardless of whether anyone asked to be forgotten.
Financial records are retained — stated plainly
Erasure does not delete financial transaction records. Records of purchases, subscriptions, renewals, and refunds that originate from your payment rails — Stripe, Apple, Google — survive a forget() request.
Why. Legal and accounting retention obligations for financial transactions (tax law, bookkeeping statutes, audit requirements) generally supersede erasure rights — GDPR Article 17(3)(b) itself carves out processing required for compliance with a legal obligation. A payment that actually happened is a fact your books, and your payment processor's books, are required to reflect for years. Crossdeck mirrors that reality rather than pretending otherwise: the behavioural and identity data goes, the financial record of the transaction stays.
If you paste this page to your DPO, this is the paragraph they will care about most. Your own privacy policy should state the same exception: erasure removes analytics, profile, and entitlement data; transaction records are retained for the legally required period. Crossdeck also retains the erasure audit entry itself — see below — because the record that you deleted someone's data is the evidence that you complied.
How to request erasure
All four surfaces hit the same endpoint: POST /v1/identity/forget.
Web
await Crossdeck.forget();
Sends the strongest identity hint the SDK holds (customer ID, else user ID, else anonymous ID) to schedule the server-side deletion, then wipes all local state — identity, entitlements, queue, super-properties, persistent stores. The local wipe runs even if the server call fails: the person just asked to be forgotten, and refusing to clear their device because of a network hiccup would be the wrong call. The failure is recorded in the SDK's debug stream so you can follow up server-side.
iOS / Swift
try await cd.forget() // cd is your started Crossdeck instance
Same contract: POSTs every identity hint the SDK holds, then always runs the local cleanup — identity, entitlements, super-properties, breadcrumbs — regardless of the server outcome. Outright network failure throws so you can show "couldn't reach Crossdeck — try again" UI, but the local wipe completes even on the throw path.
Node (your backend)
const result = await crossdeck.forget({ userId: "user_847" });
// result = { object: "forgot", crossdeckCustomerId, queuedAt, env }
The hints object accepts any of customerId, userId, anonymousId. Because the Node SDK authenticates with your secret key, it carries full erasure authority — this is the recommended path for erasing identified users (see the authority model).
REST
curl https://api.cross-deck.com/v1/identity/forget \
-H "Authorization: Bearer cd_sk_live_…" \
-H "Content-Type: application/json" \
-d '{"userId": "user_847"}'
// 200
{
"object": "forgot",
"crossdeckCustomerId": "cdcust_…",
"queuedAt": 1765432100000,
"env": "production"
}
Authenticate with Authorization: Bearer <key> or a Crossdeck-Api-Key header. The body requires at least one of customerId, userId, anonymousId; an optional idToken carries the identity proof when you call with a publishable key. crossdeckCustomerId in the response is the customer the server resolved and marked — it is null when no known customer matched the hints (still a 200; see Idempotency).
The Web and iOS SDKs run on your publishable key and do not currently attach an ID token to forget(). If the SDK holds an identified user, the server refuses the server-side erasure with 401 identity_token_invalid — by design, see below. The device-local wipe still completes. To finish the job for identified users, call forget() from your backend with your secret key (Node SDK or REST) as part of your account-deletion flow.
Who may erase what
Erasure is destructive, so the authority bar scales with what is being erased:
| Request | Required proof | Why |
|---|---|---|
Erase an identified customer (customerId or userId hint) |
A secret key (cd_sk_*) — or a publishable key plus a verified ID token whose sub equals the userId |
A publishable key ships in your client and is public by definition. Without a proof requirement, anyone who knows (or guesses) a user ID could destroy that user's data. The secret key proves the request comes from your backend, which has the user's explicit consent; the ID token proves it comes from the actual signed-in user. |
Erase an anonymous-only session (anonymousId hint, no identified customer attached) |
None beyond a valid API key | Anyone holding the anonymous session is that session — there is no stronger proof available, and no identified person to impersonate. The audit entry honestly records the weaker provenance as self_asserted. |
The ID-token path accepts a JWT from an auth provider your project has explicitly registered (Firebase Auth today; the issuer must be configured on your project — Crossdeck never trusts an unregistered issuer). The token's signature is verified against the provider's published keys, its audience and expiry are checked, and its sub claim must equal the userId being erased. Only a complete match counts. See Identity verification for how to register an issuer.
A publishable-key request that targets an identified customer without a valid token is rejected with 401 identity_token_invalid and the message tells you both remedies: send an ID token signed for this user, or call from your backend with the secret key.
Timeline — what happens, when
Immediately, in one transaction
When the request is accepted, a single atomic transaction:
- stamps the customer record with
forgottenAt(now) andforgetDeadlineAt(now + 30 days), - appends the
erasure_executedaudit entry to the identity journal, and - writes the durable deletion-queue row —
requestedAt,deadlineAt, the resolved customer ID, the original hints, andprocessedAt: null.
All three land together or none at all. If the transaction fails you get a 500 with "retry the call" — the request was not partially recorded.
The 30-day deadline
GDPR requires erasure "without undue delay and in any event within one month". Crossdeck encodes that as a hard 30-day SLA, computed once at request time and written down in two places — the customer record and the queue row. It never moves. Because every queue row carries deadlineAt next to processedAt, an overdue request — deadline passed, processedAt still null — is mechanically identifiable from the data itself, and Crossdeck support can produce the exact queue row and its timestamps for any request you need to account for.
Asynchronous deletion
forget() returns as soon as the request is durably recorded, not when the deletion finishes — erasing a customer's history can touch thousands of rows across storage systems. The queued work covers the customer's stored event rows, identity audit rows, and entitlement rows. Two categories are explicitly outside the deletion's scope:
- Rail transaction records — retained for legal/accounting reasons, as stated above.
- The erasure audit entry itself — retained as the demonstrable record that the erasure was requested and executed.
If the hints don't match anyone
If identity resolution fails at request time — say the user was never identified to Crossdeck, or resolution itself errored — the request is not dropped. The queue row is still written, keyed on whichever hints you sent, and resolution is re-attempted at processing time. An erasure request never silently evaporates because the lookup was unlucky on the first try.
Idempotency
forget() is safe to call more than once for the same person:
- The audit trail collapses to one decision. The journal entry's idempotency key is derived from the resolved customer (
forget:<customerId>, or the raw hint when unresolved). A repeat call finds the existing entry and returns it unchanged — no duplicate audit row, no double-counted erasure. - A repeat call queues a fresh deletion-queue row, which is harmless: deleting already-deleted rows is a no-op.
- No match is still success. A request whose hints resolve to no known customer returns
200withcrossdeckCustomerId: null. From the caller's perspective the contract is "this person's data is scheduled for deletion" — and for a person Crossdeck has no record of, that is already true.
The audit trail
Every erasure is recorded in the identity journal — Crossdeck's append-only audit log for every identity decision. The journal's properties are what make the erasure record usable as compliance evidence:
- Append-only. Entries are written once; no update path exists, and the collection is locked to server-side writes.
- Hash-chained. Each entry carries the SHA-256 of the previous entry. Tampering with any earlier entry invalidates every hash that follows; a verification pass walks the chain and reports the first breach, if any.
- Sequence-numbered. A monotonic per-project counter, so the chain can be enumerated in order with no timestamp ambiguity.
- Evidenced. Every entry records the provenance of the authority behind it.
The erasure_executed entry specifically records:
| Field | What it captures |
|---|---|
decision | erasure_executed |
inputs.hints | The identifiers the request supplied (user ID and/or anonymous ID). The audit entry retains these — they are the legal record of whose data was erased. |
outputs | The resolved customer ID (or "(unresolved)") and the fields stamped on the record (forgottenAt, forgetDeadlineAt). |
evidence | The authority: a verified ID token (issuer + subject), internal_admin for a secret-key call from your backend, or self_asserted for an anonymous-only erasure. |
caller | The calling surface (sdk:v1/identity/forget), the request's IP address, and user agent. |
timestampMs, sequenceNumber, entryHash, previousHash | The when, the position in the chain, and the tamper-evidence. |
If a regulator or your DPO asks "prove this user was erased, when, and on whose authority", this entry — plus the chain verification around it — is the answer. It is what Crossdeck support will show you for any erasure dispute.
What the dashboard shows after erasure
On the customer's detail page, the Audit timeline — the dashboard's rendering of the identity journal — shows the erasure as "Personal data erased (GDPR)", alongside the evidence kind behind it, in sequence with every other identity decision ever made about that customer. The timeline is read from the same journal described above, so what you see in the dashboard is the audit record, not a separate summary of it.
Related
- Identify users — how identity hints (user ID, anonymous ID) resolve to the canonical customer record that
forget()targets. - Identity verification — registering your auth provider so ID tokens can be verified, the same mechanism the erasure authority check uses.
- API reference — the full request/response schema for
POST /v1/identity/forgetand the error codes it can return.