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
| Signal | Standard | Trigger | What you receive |
|---|---|---|---|
account-purged Security Event Token (primary) | OpenID SSF / RISC + RFC 8417 SET, delivered via SET Push (RFC 8935) | Subject erasure completes | A signed JWT (application/secevent+jwt) POSTed to your registered SSF receiver |
| Back-Channel Logout (session-end) | OpenID Connect Back-Channel Logout 1.0 | Subject erasure completes | A logout_token POSTed to each RP's backchannel_logout_uri |
| Outbound SCIM deprovisioning | SCIM 2.0 (RFC 7644) | Subject erasure completes | A 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:
| Field | Meaning |
|---|---|
deliveryMode | How SETs reach you: push (default), poll, or webhook. See Choose a delivery mode below. |
push_url | Your 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). |
audience | The aud value Thoryn stamps in the SET / webhook — your receiver identifier. Validate it on receipt. |
client_id | Optional — 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_id | Optional — 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. |
enabled | Toggle delivery without removing the registration. |
The management endpoints (all under the tenant's tnt isolation; a cross-tenant
id returns 404, never 403):
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST | /api/v1/ssf/receivers | tenant:compliance.write | Register a receiver (re-posting the same push_url updates it in place). |
GET | /api/v1/ssf/receivers | tenant:compliance.read | List the tenant's receivers. |
GET | /api/v1/ssf/receivers/{id} | tenant:compliance.read | Fetch one receiver. |
PATCH | /api/v1/ssf/receivers/{id} | tenant:compliance.write | Enable / disable ({"enabled": false}). |
DELETE | /api/v1/ssf/receivers/{id} | tenant:compliance.write | Remove 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):
deliveryMode | Standard | How you receive SETs | Choose it when |
|---|---|---|---|
push (default) | SET Push, RFC 8935 | Thoryn POSTs each signed SET to your pushUrl (application/secevent+jwt). | You can expose a public HTTPS endpoint to receive pushes. |
poll | SET Poll, RFC 8936 | Thoryn 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. |
webhook | Bespoke 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 nowThen 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
setsmap 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 theackarray of your next poll. Acknowledged SETs are not returned again. maxEventscaps the batch;moreAvailable: truemeans more are queued.returnImmediately/setErrsare 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
401with 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
- Confirm the media type is
application/secevent+jwtand the token parses as a compact JWS. - 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). - Check
audmatches your receiver and read the event type from the single key of theeventsmap (see the RISC event set below — do not assumeaccount-purged; a receiver also getsaccount-disabled). - Act on your local copy of the subject per the event (deprovision on
account-purged; disable/suspend onaccount-disabled). - 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 URI | Fires when | What it means for you |
|---|---|---|
…/risc/event-type/account-purged | A subject erasure completes (the data is destroyed). | The account no longer exists. Deprovision (delete/anonymize) your local copy. |
…/risc/event-type/account-disabled | The 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-recycled | An 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:
| Field | Meaning |
|---|---|
scim_base_url | Your 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. |
credential | Optional 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_id | Optional — 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). |
enabled | Toggle 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:
- resolves the resource —
GET {base}/Users?filter=externalId eq "{sub}"; - for each match, issues
DELETE {base}/Users/{id}; - if you answer the
DELETEwith405 Method Not Allowed/501 Not Implemented(a soft-delete-only provider), falls back toPATCH {base}/Users/{id}withactive=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-purgedSETs 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, andscim_base_urlare RP-supplied and always re-validated through Thoryn'sOutboundUrlGuardbefore dispatch — they must be public HTTPS endpoints, never private-range or metadata addresses. - The SCIM
credentialyou 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_tokensignature 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.