Skip to content

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:

FieldMeaning
vetotrue to opt this client in to the pre-erasure consult. Default false.
vetoCallbackUrlThe 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:

MethodPOST
Pathyour registered vetoCallbackUrl
Content-Typeapplication/json
X-Thoryn-Veto-Signaturecompact 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"
}
FieldRequiredMeaning
vetoyestrue to place a hold (retain the subject); false to allow erasure to proceed.
reasonwhen veto is trueHuman-readable reason recorded on the hold.
legalBasiswhen veto is trueThe GDPR ground for the objection.
reviewAfteroptionalA 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:

  1. Compute digest = base64url_nopad( SHA-256( raw_request_body_bytes ) ).
  2. Split the header value on . into header, payload, signature.
  3. Confirm payload == base64url_nopad( digest ) — this binds the signature to this body.
  4. Verify the ES256 signature over the ASCII string header + "." + payload using the tenant's public key. The JWS kid (in the decoded header) 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 on request_id and 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: false promptly when you have no objection.

Security notes

  • Serve the endpoint over HTTPS; the platform refuses plain HTTP in production and never follows redirects.
  • The subject is 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 / legalBasis you return are recorded verbatim on the hold and surfaced to the tenant's data-protection workflow — do not put secrets in them.