Skip to content

Product documentation

Receive erasure signals: account-purged SETs, back-channel logout, and SCIM deprovisioning

How a relying party integrated with Thoryn is told when a tenant erases an end user (GDPR Art. 17): register an OpenID SSF receiver to get signed RISC Security Event Tokens (SET Push, RFC 8935) — account-purged on erase, account-disabled on the deactivate phase — receive OIDC Back-Channel Logout, and register a SCIM 2.0 endpoint to be deprovisioned by externalId. Includes the RISC event set, the exact SET shape, the subject→externalId assumption, and the Art. 17(2) boundary — a signal, not a guarantee.

Receive erasure signals from Thoryn

When a Thoryn tenant erases one of its end users under the GDPR right to erasure (Article 17), Thoryn already severs that subject's sessions and revokes their tokens at the broker. On top of that, Thoryn actively signals the relying parties (RPs) integrated with the tenant, so each RP can deprovision its own downstream copy of the user. This is the Article 17(2) "inform other controllers" obligation.

This page is for RP implementers. It covers the standards-first signals Thoryn emits today — an SSF/RISC Security Event Token (deliverable by Push, Poll, or a bespoke webhook fallback), OIDC Back-Channel Logout, and outbound SCIM deprovisioning — and how to receive each.

:::note The Art. 17(2) boundary — a signal, not a guarantee Thoryn signals that a subject was erased. It does not — and cannot — erase the RP's own datastore: the RP is a separate controller. Acting on the signal (deleting or anonymizing your copy of the user) is the RP's responsibility. Thoryn never claims to have erased data it does not control. :::

The three signals

SignalStandardTriggerWhat you receive
account-purged Security Event Token (primary)OpenID SSF / RISC + RFC 8417 SET, delivered via SET Push (RFC 8935)Subject erasure completesA signed JWT (application/secevent+jwt) POSTed to your registered SSF receiver
Back-Channel Logout (session-end)OpenID Connect Back-Channel Logout 1.0Subject erasure completesA logout_token POSTed to each RP's backchannel_logout_uri
Outbound SCIM deprovisioningSCIM 2.0 (RFC 7644)Subject erasure completesA DELETE (or PATCH active=false) against the erased user in your registered SCIM service, resolved by externalId

The three are complementary: the SET is the durable, machine-readable "this account no longer exists" record; back-channel logout tears down any live session immediately; and the outbound SCIM deprovision actively asks your own user store to drop the record.

1. Receive the account-purged SET (primary)

Register an SSF receiver

An SSF receiver is per-tenant configuration Thoryn stores for you: the push URL Thoryn delivers to, and the audience it stamps in the SET. A tenant admin registers and manages receivers through the customer-plane API (SSO-2523), scope-gated under the same tenant:compliance.* family as the rest of the GDPR surface. Each receiver has:

FieldMeaning
deliveryModeHow SETs reach you: push (default), poll, or webhook. See Choose a delivery mode below.
push_urlYour SSF push-delivery (or webhook) endpoint. Must be publicly reachable HTTPS; it is validated through Thoryn's outbound SSRF guard at registration time and re-validated on every delivery, so private/loopback/cloud-metadata targets are rejected. Unused for a poll receiver (pass any placeholder).
audienceThe aud value Thoryn stamps in the SET / webhook — your receiver identifier. Validate it on receipt.
client_idOptional — the OAuth client this receiver belongs to. Omit for a tenant-wide receiver that fires for every erasure in the tenant. Set it to scope the receiver to one client: the receiver then fires only for erasures of subjects that were known to that client (had a consent or authorization with it) — so you are never told about a user who only ever used a different client (GDPR data-minimisation). If Thoryn cannot resolve which clients a subject was known to, it errs toward delivering (a signal for a subject you do not recognise is a harmless no-op).
environment_idOptional — bind the receiver to a single environment of your workspace (a sandbox or production plane). Omit for a tenant-wide receiver that covers every environment. When you erase a subject, Thoryn selects the receivers scoped to that subject's environment plus every tenant-wide receiver — so a sandbox erasure never signals a production-only receiver, and vice versa. A non-null environment_id must name an environment of your tenant, else the request is rejected 400 invalid_environment.
enabledToggle delivery without removing the registration.

The management endpoints (all under the tenant's tnt isolation; a cross-tenant id returns 404, never 403):

MethodPathScopePurpose
POST/api/v1/ssf/receiverstenant:compliance.writeRegister a receiver (re-posting the same push_url updates it in place).
GET/api/v1/ssf/receiverstenant:compliance.readList the tenant's receivers.
GET/api/v1/ssf/receivers/{id}tenant:compliance.readFetch one receiver.
PATCH/api/v1/ssf/receivers/{id}tenant:compliance.writeEnable / disable ({"enabled": false}).
DELETE/api/v1/ssf/receivers/{id}tenant:compliance.writeRemove a receiver.

Register a receiver:

curl -X POST https://api.<your-thoryn-domain>/api/v1/ssf/receivers \
  -H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "pushUrl": "https://your-app.example/ssf",
        "audience": "https://your-app.example/ssf",
        "description": "Production SSF receiver"
      }'

A 201 Created echoes the stored receiver (id, enabled, environmentId, timestamps). A push URL that resolves to a private, loopback or cloud-metadata target is rejected 400 invalid_push_url up front. The full request/response schema is in the API reference. SSF receiver management is a platform-live operation, so it is closed to test-mode (sandbox) credentials.

To scope a receiver to one environment, add its environmentId to the body (omit it for a tenant-wide receiver). The same push_url may be registered once per environment — re-posting the same push_url within the same environment updates that receiver in place rather than creating a duplicate.

Choose a delivery mode: Push, Poll, or the webhook fallback

The same tenant-signed SET can reach you three ways. A receiver's deliveryMode selects which (omit it for push, the default — every existing receiver is unchanged):

deliveryModeStandardHow you receive SETsChoose it when
push (default)SET Push, RFC 8935Thoryn POSTs each signed SET to your pushUrl (application/secevent+jwt).You can expose a public HTTPS endpoint to receive pushes.
pollSET Poll, RFC 8936Thoryn queues each signed SET; you pull + acknowledge them from the poll delivery endpoint.You can't accept inbound pushes (behind a firewall / no public endpoint) and prefer to pull.
webhookBespoke account.erased webhook (fallback)Thoryn POSTs a signed account.erased payload to your pushUrl (application/jwt).You support neither SSF nor OIDC back-channel logout — the last-resort fallback.

Push is the primary, standards-first path. Reach for Poll when you can't receive pushes, and the webhook only as a fallback when SSF is not an option at all.

Poll delivery (RFC 8936)

Register a poll receiver. Because a poll receiver pulls, it has no push URL Thoryn dispatches to (pass any placeholder pushUrl; it is not used and not SSRF-validated), and Thoryn mints a poll token you authenticate the poll endpoint with. The token is returned once, in the registration response, and is stored only Vault-Transit envelope-encrypted — it is never returned on a read. Store it now.

curl -X POST https://api.<your-thoryn-domain>/api/v1/ssf/receivers \
  -H "Authorization: Bearer $TENANT_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "deliveryMode": "poll",
        "pushUrl": "https://unused.example/poll",
        "audience": "https://your-app.example/ssf"
      }'
# 201 Created
# { "id": "...", "deliveryMode": "poll", "pollCredentialConfigured": true,
#   "pollToken": "ssfpoll_XXXXXXXX..." }   <-- shown ONCE; store it now

Then poll for queued SETs, presenting the poll token as a bearer:

curl -X POST https://api.<your-thoryn-domain>/api/v1/ssf/poll/<receiverId> \
  -H "Authorization: Bearer ssfpoll_XXXXXXXX..." \
  -H "Content-Type: application/json" \
  -d '{ "maxEvents": 10, "returnImmediately": true, "ack": ["<id-from-a-prior-poll>"] }'
# 200 OK
# {
#   "sets": { "<deliveryId>": "<compact-JWS SET>", ... },
#   "moreAvailable": false
# }
  • The response sets map is keyed by a delivery id Thoryn assigns to each queued SET (RFC 8936 §2.4). Verify + process each SET (identical shape to a pushed SET, below), then acknowledge it by echoing its id in the ack array of your next poll. Acknowledged SETs are not returned again.
  • maxEvents caps the batch; moreAvailable: true means more are queued.
  • returnImmediately / setErrs are accepted for wire compatibility; this transmitter always returns immediately (it does not long-poll).
  • Authentication is the poll token only — the poll endpoint takes no Thoryn tenant token. A missing / wrong token, or a receiver that isn't a poll receiver, returns 401 with no existence oracle. Each (receiverId, token) pair only ever sees its own queue.

Re-registering a poll receiver (a POST with the same pushUrl + environment) rotates the poll token and returns the new one; toggling it with PATCH preserves the existing token.

The webhook fallback (account.erased)

If your platform supports neither SSF nor OIDC back-channel logout, register a webhook receiver. On erasure Thoryn POSTs a signed account.erased payload to your pushUrl as application/jwt — a compact JWS signed with the same tenant ES256 key as the SET (verify it the same way):

// Payload of the account.erased webhook JWS
{
  "iss": "https://hub.<your-thoryn-domain>",
  "jti": "…",
  "iat": 1765400000,
  "aud": "https://your-app.example/ssf",   // your receiver `audience`
  "txn": "<erasure request id>",
  "event": "account.erased",
  "subject": { "format": "opaque", "id": "<pseudonymous subject>" }
}

The webhook pushUrl is RP-supplied, so it is SSRF-validated at registration and before every dispatch, exactly like a push receiver. This is the fallback: prefer SSF (Push or Poll) whenever you can.

What the SET looks like

Thoryn POSTs the compact JWS to your push_url with Content-Type: application/secevent+jwt. Decoded, the token is:

// Header
{
  "alg": "ES256",
  "typ": "secevent+jwt",
  "kid": "v3:dsar-bundle-<tenantId>"   // key version + tenant key name
}
// Payload
{
  "iss": "https://hub.<your-thoryn-domain>",
  "jti": "5f2b…",                       // unique SET id
  "iat": 1754899200,
  "aud": "https://your-app.example/ssf",
  "sub_id": { "format": "opaque", "id": "<pseudonymous platform subject>" },
  "txn": "<erasure request id>",        // correlation
  "events": {
    "https://schemas.openid.net/secevent/risc/event-type/account-purged": {
      "subject": { "format": "opaque", "id": "<pseudonymous platform subject>" }
    }
  }
}

The subject is the pseudonymous Thoryn sub you already received in that user's ID tokens — no PII rides in the SET. Match it against your stored mapping to find the local user to deprovision.

Verify and respond

  1. Confirm the media type is application/secevent+jwt and the token parses as a compact JWS.
  2. Verify the ES256 signature against the tenant's published key (resolved from the kid). The SET is signed with the same per-tenant Thoryn signing key that signs DSAR bundles and consent receipts — provisioned to you out of band today (a transmitter JWKS endpoint is a documented follow-up).
  3. Check aud matches your receiver and read the event type from the single key of the events map (see the RISC event set below — do not assume account-purged; a receiver also gets account-disabled).
  4. Act on your local copy of the subject per the event (deprovision on account-purged; disable/suspend on account-disabled).
  5. Reply 202 Accepted (per RFC 8935). Any non-2xx is treated as a delivery failure; delivery is best-effort and isolated per receiver.

The RISC event set — which lifecycle events arrive as SETs

A single SSF receiver receives every RISC event Thoryn emits for the tenant (there is no per-event subscription today). The event type is the sole key of the events map — switch on it. Thoryn emits:

RISC event type URIFires whenWhat it means for you
…/risc/event-type/account-purgedA subject erasure completes (the data is destroyed).The account no longer exists. Deprovision (delete/anonymize) your local copy.
…/risc/event-type/account-disabledThe deactivate phase of a conditional erasure — the subject is suspended (sessions + tokens revoked, SCIM active=false) but data is not yet destroyed.The account is disabled. Stop processing / suspend your local copy; a later account-purged may follow once any legal hold clears.
…/risc/event-type/identifier-recycledAn identifier previously held by an erased subject is reassigned to a different subject — i.e. a new user registers with an email a prior/erased user released (SSO-2531). Unlike the two events above, the SET subject is the identifier itself (the recycled email), so you match on the identifier, not on the old subject's id.An identifier you saw for one subject now belongs to a different subject. Break any local association keyed on that identifier before you link it to the new account.

All three share the identical signed-SET wire shape above — only the events key (the event-type URI) and the firing point differ. account-disabled is the earlier, reversible signal in the two-phase erasure pipeline; account-purged is the terminal one. Treating account-disabled as a soft-disable and account-purged as the hard-delete keeps your store aligned with Thoryn's lifecycle even when an erasure is held open by a legal obligation.

:::note Order and idempotency For a conditional erasure you may receive account-disabled first and account-purged later (possibly much later, after a hold window). For a break-glass immediate erasure you receive only account-purged. Both events are best-effort and may be redelivered — make your handler idempotent per sub_id. :::

2. Receive Back-Channel Logout

If your client registered a backchannel_logout_uri, Thoryn also POSTs a signed logout_token (form field logout_token) when the subject is erased. The token carries the subject and omits sid, which per the OIDC Back-Channel Logout spec means "log the subject out of all sessions" — the whole account is being erased, not one session. Validate the logout_token exactly as you would for an ordinary back-channel logout (issuer, audience, events claim), then terminate the subject's sessions.

The logout_token's iss is the effective issuer of the tenant — the exact issuer you saw at login. For a tenant on a verified custom domain that is https://<custom-domain>; otherwise it is the standard subdomain issuer https://<slug>.<your-thoryn-domain>. Validate against the issuer your client is configured for, as usual.

3. Receive an outbound SCIM deprovision

If your application exposes a SCIM 2.0 service (RFC 7644), you can register its base URL so Thoryn actively deprovisions the erased user from your store — the outbound counterpart to the inbound SCIM you may already use to provision users into Thoryn. A tenant admin registers and manages endpoints through the customer-plane API (SSO-2520), under the same tenant:compliance.* scope family as the rest of the GDPR surface. Each endpoint has:

FieldMeaning
scim_base_urlYour SCIM 2.0 base URL (e.g. https://rp.example/scim/v2). Must be publicly reachable HTTPS; validated through Thoryn's outbound SSRF guard at registration time and re-validated before every deprovision, so private/loopback/cloud-metadata targets are rejected.
credentialOptional SCIM bearer Thoryn presents (Authorization: Bearer …). Vault-Transit envelope-encrypted at rest; never returned on any read (a credential_configured boolean is surfaced instead). Omit for an unauthenticated / mTLS-fronted endpoint.
client_idOptional — the OAuth client this endpoint belongs to. Omit for a tenant-wide endpoint that is asked to deprovision every erased subject. Set it to scope the endpoint to one client: it is then asked to deprovision only subjects that were known to that client (had a consent or authorization with it), matching the SSF-receiver scoping above. If Thoryn cannot resolve the subject↔client linkage it errs toward deprovisioning (a no-match SCIM lookup is a harmless no-op).
enabledToggle deprovisioning without removing the registration.

Management endpoints live under /api/v1/scim-deprovision/endpoints (POST / GET / GET {id} / PATCH {id} / DELETE {id}), tenant-isolated — a cross-tenant id returns 404, never 403.

How Thoryn addresses the resource — the externalId assumption

:::note Assumption: your SCIM externalId is the Thoryn subject Thoryn keys the deprovision on the erased subject's pseudonymous platform subject (the OIDC sub your client already receives in tokens), and assumes that value is the externalId on your SCIM User resource. If you set externalId to the Thoryn sub when you provision the user, deprovisioning resolves automatically. If your store has no resource with that externalId, the deprovision is a best-effort no-op (recorded, never an error). :::

For each enabled endpoint Thoryn:

  1. resolves the resource — GET {base}/Users?filter=externalId eq "{sub}";
  2. for each match, issues DELETE {base}/Users/{id};
  3. if you answer the DELETE with 405 Method Not Allowed / 501 Not Implemented (a soft-delete-only provider), falls back to PATCH {base}/Users/{id} with active=false.

A 404 on the DELETE is treated as success — the goal (the record is gone) is already met.

Delivery semantics

  • Best-effort, per-target isolated. A failing receiver, logout endpoint, or SCIM endpoint never blocks or reverses the erasure, and never affects delivery to the other targets. Failures are recorded on Thoryn's retained erasure audit trail.
  • No PII. Only the pseudonymous platform subject is transmitted.
  • Idempotent handling recommended. Treat repeated account-purged SETs for the same subject as a no-op.

Not yet delivered (roadmap)

These are deliberately out of scope for the current release and tracked as follow-ups (ADR 2026-08-11-gdpr-trust-center-and-consent-records, Decision 4b):

  • An SSF-transmitter JWKS discovery endpoint — today you verify SETs against the tenant's published key out of band.

(SET Poll delivery, the account.erased webhook fallback, the RISC account-disabled event, and the RISC identifier-recycled event — fired on identifier reassignment, SSO-2531 — are now delivered; see the delivery-mode section above, the deactivate-phase SET, and the RISC event table.)

Security notes

  • Your push_url, backchannel_logout_uri, and scim_base_url are RP-supplied and always re-validated through Thoryn's OutboundUrlGuard before dispatch — they must be public HTTPS endpoints, never private-range or metadata addresses.
  • The SCIM credential you register is Vault-Transit envelope-encrypted at rest, decrypted only in memory for the single deprovision call, and never logged or returned on any read.
  • Always verify the SET / logout_token signature before acting; never deprovision on an unverified request.
  • The signal is authenticated proof the request came from Thoryn; it is not a legal transfer of the erasure obligation. You remain the controller of your own copy of the data.