Patterna Docs

Handle a deletion request

Delete or export everything Patterna holds about one user, and prove when it happened.

Developers 5 min read View as Markdown

When a user deletes their account or asks for their data, your backend makes one call. Patterna knows the user only by the random id you minted, so you look that id up on your side first.

Setup for the code examples#

Every example below comes in four tabs. The TypeScript and Go tabs use the SDKs for serve and ingest calls. The SDKs have admin methods too (applyFlow, setSurfaceStatus, ledger and so on), but these tabs call the admin REST API through the small rest helper defined here so every request is visible. The Python tab calls the REST API with httpx; the Python SDK (patterna-sdk, not on PyPI yet: install it from the public repository payam-h/patterna-sdk) has the same shape as the other two. The Go snippets run inside a function that has ctx context.Context and pc *patterna.Client from newClient() and returns an error; add time to the imports where a snippet uses it.

# Keys come from Connectors or Governance. Each is shown once.
# Serve keys start with pk_s_, ingest keys with pk_i_, admin keys with pk_a_.
export PATTERNA_SERVE_KEY="pk_s_paste-the-serve-key"
export PATTERNA_INGEST_KEY="pk_i_paste-the-ingest-key"
export PATTERNA_ADMIN_KEY="pk_a_paste-the-admin-key"

Delete a user#

Deletion takes the ingest key, the same key your backend already holds. An admin key works too.

curl -sS -X DELETE "https://patterna-api.orven.am/v1/users/u_mfrggzdfmztwq2lknnwg23tpobyxe" \
  -H "Authorization: Bearer $PATTERNA_INGEST_KEY"

The answer is 202 Accepted with the deletion request. The rows are already gone when it arrives:

JSON
{
  "id": "6f1c2b9e-3d4a-4f0e-9a51-2c7d8e1b0a44",
  "user_id": "u_mfrggzdfmztwq2lknnwg23tpobyxe",
  "requested_at": "2026-10-13T09:12:00Z",
  "rows_deleted_at": "2026-10-13T09:12:00Z",
  "models_retrained_at": null,
  "stored_as": "sha256"
}

stored_as: "sha256": the deletion record keeps a salted hash of the id, not the id, so the record of a deletion is not itself personal data.

The SDKs wrap this call: deleteUser(userId) in TypeScript, DeleteUser(ctx, id) in Go.

What happens next#

Deletion is synchronous. In one transaction, before the answer, the engine:

  • deletes the user row, the holdout assignments, the opt-out marker, the user's events and precomputed shortlist;
  • unlinks the user's impressions: the user id is removed and the logged candidate list emptied. The slates stay in the ledger under their unit_hash (a SHA-256 of a secret salt Patterna holds per tenant and the user id; the salt is set once and never rotated, so the hash cannot be reversed and someone who holds a user id cannot check whether a row belongs to it), so ledger counts and intervals do not change after the fact.

rows_deleted_at is therefore set in the answer. One timestamp is left:

  • models_retrained_at. Model artifacts learned from the user's data are cleared through the retrain schedule. The nightly learning run (02:00 UTC) stamps this field on every open request once it has retrained without the deleted rows.

Governance → deletion requests shows every request with both timestamps. Audiences → trace lookup shows the status for a single id.

Export a user's data#

curl -sS "https://patterna-api.orven.am/v1/users/u_mfrggzdfmztwq2lknnwg23tpobyxe" \
  -H "Authorization: Bearer $PATTERNA_ADMIN_KEY"

The answer is every row Patterna holds about that id, as JSON. Hand it to the user together with your own export.

When a user withdraws consent for personalized recommendations but keeps the account, set their mode to non_profiled:

curl -sS -X PUT "https://patterna-api.orven.am/v1/users/u_mfrggzdfmztwq2lknnwg23tpobyxe/mode" \
  -H "Authorization: Bearer $PATTERNA_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "non_profiled" }'

Switching to non_profiled is an opt-out, with the same semantics as a deletion of the profile. In the request the user row (profile and affinities) is deleted, Patterna stores a salted hash of the id as the opt-out marker, and it files a deletion request that holds only that salted hash. The hourly deletions job then unlinks the user's impressions (the id and the logged candidates are cleared; the salted unit_hash stays, so ledger rows do not change after the fact), keeps their interactions (they belong to the slate), and deletes the user's events, holdout assignments and precomputed shortlist. Serve honours the marker at once: a personalized or cohort request for that id is served as non_profiled and logged without the id, keeping the unit it had and, unless the holdout is stratified by persona (the user has no persona any more), the arm; the audit log records serve.optout_downgrade once per day, without the id. Setting the mode to personalized or cohort again (with this call, or a profile that carries mode) clears the marker. The nightly retrain clears the models.

Opt-out (PUT /users/{id}/mode non_profiled)Deletion (DELETE /users/{id})
User row and profiledeleted in the requestdeleted in the request
Events, holdout assignments, shortlistdeleted by the hourly deletions jobdeleted in the request
Impressionskept, unlinked by the hourly jobkept, unlinked in the request
Interactionskept with their slatekept with their slate
Servingat once: profiled requests for the id are served as non_profiledthe id is unknown; a new profile needs mode
What Patterna keeps about the ida salted hash, as the opt-out marker serve honoursa salted hash in the deletion record; the opt-out marker is removed
Modelsforget at the next nightly retrainforget at the next nightly retrain (models_retrained_at)

From then on the user is served on a rotating session id only. See Consent modes.

The pattern on your side#

This pattern keeps deletion reliable without keeping personal data:

  • Keep the mapping on your side. A table maps your account id to the random Patterna id. Patterna never sees your account id or an email.
  • Tell Patterna first, then delete the mapping. Look up the Patterna id, call delete, then remove the account and its mapping row.
  • Never block the user's deletion. If the call fails, store only the random id in a retry table and delete the account anyway. Once the mapping row is gone the random id points to nobody.
  • Retry until confirmed. A loop (every 10 minutes is plenty) resends pending deletions and removes each id once Patterna answers.
  • Rotate on revoke. After a successful switch to non_profiled, delete the mapping row. A later opt-in mints a fresh id that cannot be joined to the old history.
  • One path for every deletion. Staff tools and retention jobs that delete users call the same erase function.

Which key#

CallKey
DELETE /users/{user_id}ingest (pk_i_…) or admin
PUT /users/{user_id}/mode, PUT /users/{user_id}/profileingest or admin
GET /users/{user_id} (export), GET /users/{user_id}/traceadmin (pk_a_…)

A customer backend needs no admin key for consent and deletion. Both SDKs send the admin key when one is configured and the ingest key otherwise; configure only the ingest key on a backend.

esc
  • Type to search every page, heading and code sample.