Product documentation
Pre-erasure veto callback: implementing a veto endpoint
How a relying party opts in as a veto client and implements the pre-erasure veto callback — the signed request/response contract, verifying the Thoryn signature, idempotency, retries, and the fail-closed guarantee that keeps a subject's data from being erased while your objection stands.
Pre-erasure veto callback
When a Thoryn tenant erases an end user (GDPR Art. 17 — right to erasure), the platform runs a two-phase, hold-checked pipeline: it first deactivates the identity (sessions and tokens are revoked, no data is destroyed), waits out a grace window, and only then hard-erases. During the hold-check phase the platform consults every veto client the tenant has registered, so a relying party that has a lawful reason to retain the subject a little longer — an open dispute, a statutory retention obligation, an in-flight financial settlement (GDPR Art. 17(3)) — can say so before the data is gone.
Only clients that opt in are consulted. Every other client is simply erased and (separately) notified. If you do not implement a veto endpoint, nothing changes for you.
For tenant admins: flag your application as a veto client
Set two fields on the OAuth client (application) registration, through the console or the
/api/v1/applications API:
| Field | Meaning |
|---|---|
veto | true to opt this client in to the pre-erasure consult. Default false. |
vetoCallbackUrl | The https:// endpoint the platform POSTs the signed veto request to. Required when veto is true. |
PATCH /api/v1/applications/{clientId}
Authorization: Bearer <tenant-admin token>
Content-Type: application/json
{ "veto": true, "vetoCallbackUrl": "https://rp.example.com/thoryn/pre-erasure-veto" }The callback URL is validated for SSRF on every call (no private, loopback, link-local, or cloud-metadata address; HTTPS in production) and redirects are never followed.
For RP implementers: the callback contract
Request
The platform POSTs the following to your vetoCallbackUrl:
| Method | POST |
| Path | your registered vetoCallbackUrl |
Content-Type | application/json |
X-Thoryn-Veto-Signature | compact ES256 JWS over the request digest (see Verifying the signature) |
{
"subject": "8f1c…",
"tenant": "b3e2…",
"request_id": "0d9a…",
"timestamp": "2026-08-11T14:24:46Z"
}subject— a pseudonymous identifier for the end user being erased (the platform's internal user id, not an email or other PII).tenant— the tenant (workspace) id the erasure belongs to.request_id— the erasure request id. Use this as your idempotency key (see below).timestamp— ISO-8601 UTC time the request was signed.
Response
Reply 200 OK with:
{
"veto": true,
"reason": "Open chargeback dispute #77",
"legalBasis": "GDPR Art. 17(3)(e) — establishment/exercise/defence of legal claims",
"reviewAfter": "2026-12-01"
}| Field | Required | Meaning |
|---|---|---|
veto | yes | true to place a hold (retain the subject); false to allow erasure to proceed. |
reason | when veto is true | Human-readable reason recorded on the hold. |
legalBasis | when veto is true | The GDPR ground for the objection. |
reviewAfter | optional | A date/time hint for when the objection may no longer apply. Advisory — it does not extend the hold on its own. |
A veto: false (or the absence of any veto client) lets the erasure proceed once the grace window
elapses.
What a veto does — and does not — do
A veto places a hold: the subject is not erased while your objection stands. The platform
re-consults you on a schedule, so as soon as you reply veto: false, the hold clears and erasure
proceeds. A hold is never indefinite — it is bounded by a maximum hold window, after which the
request is escalated to the tenant's data-protection contact rather than silently erased or silently
dropped. A veto buys lawful time; it is not a permanent block.
Verifying the signature
Every request is signed with the tenant's own key (per-tenant ES256), so you can prove the request
genuinely came from Thoryn for that tenant. The X-Thoryn-Veto-Signature header is a compact JWS
(header.payload.signature) whose payload is base64url(digest), where digest = base64url(SHA-256(raw request body bytes)).
To verify:
- Compute
digest = base64url_nopad( SHA-256( raw_request_body_bytes ) ). - Split the header value on
.intoheader,payload,signature. - Confirm
payload == base64url_nopad( digest )— this binds the signature to this body. - Verify the ES256 signature over the ASCII string
header + "." + payloadusing the tenant's public key. The JWSkid(in the decodedheader) carries the key version so you resolve the exact key. Retrieve the tenant's public key from the tenant's published JWKS.
Because the signature covers a digest of the exact bytes we send, verify against the raw body you received — do not re-serialize the JSON first.
Idempotency and retries
- Idempotency. The platform re-consults veto clients across the hold window, and may retry after a
transient failure, so your endpoint will be called more than once for the same
request_id. Make it idempotent: key your decision onrequest_idand return the same answer for repeat calls. - Timeout. Respond within the callback timeout (5 seconds by default). A slow endpoint is treated the same as an unreachable one.
- Fail-closed. If your endpoint is unreachable, times out, errors, or returns a response that
cannot be parsed, the platform fails closed — it places a hold ("veto-client unreachable") rather
than proceeding to erase. Erasure is deferred, never wrongly performed. Keep your endpoint healthy so
a legitimate erasure is not held longer than necessary, and reply
veto: falsepromptly when you have no objection.
Security notes
- Serve the endpoint over HTTPS; the platform refuses plain HTTP in production and never follows redirects.
- The
subjectis pseudonymous — do not expect an email or other directly-identifying value. - Always verify the signature before acting on a request; treat an unsigned or invalid-signature request as untrusted.
reason/legalBasisyou return are recorded verbatim on the hold and surfaced to the tenant's data-protection workflow — do not put secrets in them.