Product documentation
DSAR relying-party data return
Register a per-Application data-return endpoint so a GDPR Article 15 access bundle can fan in over your relying-party apps and include the subject's data held by them — the contract your endpoint implements, the signed request Thoryn sends, and the JSON response it expects.
DSAR relying-party data return
A GDPR Article 15 (Data Subject Access Request) access bundle assembled by Thoryn's DSAR orchestrator normally fans in over the data Thoryn itself holds for the subject — the hub's protocol state, identity-service's user record, product-api's customer-plane records, and the directory projection. It does not reach into the data your relying-party (RP) apps hold about that subject.
This guide covers the symmetric counterpart of the erasure RP-propagation signals (SSF/RISC and outbound SCIM): a per-Application data-return endpoint you register so a DSAR access bundle can also include the subject's data held by your app. It is the read-side mirror of the erasure signal — where erasure fans a signal out to your app, data-return fans a read out to it and folds the response into the bundle.
For end users (tenant admins)
- In the console, open Compliance → DSAR data-return endpoints (or call the API below directly).
- Register one endpoint per Application (OAuth client) whose data should appear in access bundles: the client id, your endpoint URL (a public HTTPS URL), and optionally a bearer credential.
- On every DSAR request Thoryn resolves which of your Applications the subject was known to and calls only those endpoints. Your endpoint returns the subject's data as JSON; Thoryn folds it into the signed bundle.
A DSAR data-return endpoint is a create-once resource per
(tenant, environment, client): re-registering the same client in the same
environment returns 409. To change an endpoint, delete it and register again.
Endpoint reference (management API)
All management endpoints are on the customer plane behind the API gateway and require the compliance scopes. The surface is closed to test-mode credentials — DSAR is a platform-live right.
| Method | Path | Scope | Description |
|---|---|---|---|
POST | /api/v1/dsar-return-endpoints | tenant:compliance.write | Register an endpoint (409 on a duplicate client/environment). |
GET | /api/v1/dsar-return-endpoints | tenant:compliance.read | List the tenant's endpoints. |
GET | /api/v1/dsar-return-endpoints/{id} | tenant:compliance.read | Read one endpoint. |
PATCH | /api/v1/dsar-return-endpoints/{id} | tenant:compliance.write | Enable / disable an endpoint. |
DELETE | /api/v1/dsar-return-endpoints/{id} | tenant:compliance.write | Remove an endpoint. |
Register body:
{
"clientId": "acme-app",
"endpointUrl": "https://acme.example.com/thoryn/dsar-return",
"credential": "optional-rp-bearer",
"environmentId": null,
"description": "Acme order history + preferences"
}clientId— required. The RP OAuth client this endpoint serves.endpointUrl— required. A public HTTPS URL; private, loopback and cloud-metadata targets are rejected at registration (400 invalid_endpoint_url).credential— optional. When present the endpoint is stored asauth_mode = bearerand Thoryn attachesAuthorization: Bearer <credential>to each call. The value is Vault-Transit envelope-encrypted at rest and never returned on any read (credentialConfiguredreports only whether one is on file). When omitted the endpoint isauth_mode = signed_requestand authenticity rests solely on the request signature below.environmentId— optional. Null = a tenant-wide endpoint (covers every environment). A non-null value scopes it to that one environment and must name an environment of your tenant.
The contract your endpoint implements
Thoryn calls your endpointUrl with a signed POST and expects a JSON body of
the subject's data in response.
Request Thoryn sends
- Method:
POST {endpointUrl} Content-Type: application/jwt- Body: a compact JWS (
header.payload.signature) signed with your tenant's ES256 key — the same per-tenant signing identity Thoryn uses for the DSAR bundle and the RISC erasure SET (kidcarries the key versionv<n>:<keyName>). Authorization: Bearer <credential>— present only when the endpoint was registered with a credential (auth_mode = bearer).
The JWS payload claims:
{
"iss": "https://hub.thoryn.io",
"jti": "b1e5…",
"iat": 1765540000,
"txn": "b7c2…",
"request_type": "dsar-data-return",
"tenant": "0e2f…",
"sub_id": { "format": "opaque", "id": "<pseudonymous platform subject>" }
}sub_id.idis the pseudonymous platform subject (the hubsub) — the same identifier your app received in the ID token / userinfo for that user. Resolve your records by it. No PII rides in the request.request_typeis alwaysdsar-data-return.
Verify the request before responding: validate the ES256 signature against your
tenant's published signing key and confirm request_type is dsar-data-return. If
you registered a bearer credential, also check the Authorization header.
Response Thoryn expects
200 OK,Content-Type: application/json- A JSON object with a
recordsarray (a bare array is also accepted). Each record is a free-shape JSON object of the subject's data your app holds:
{
"records": [
{ "order_id": "A-1001", "total": "42.00", "placed_at": "2026-01-04T10:00:00Z" },
{ "preference": "newsletter", "value": "opted-out" }
]
}Return only that subject's own data. If your app has no record for the subject,
return { "records": [] } (or 200 with an empty body) — that is a clean "nothing
held", not an error.
Each returned record is wrapped in the bundle with provenance
(record_kind: "rp_return", your client_id, and the endpoint_id) under the
relying-parties module fragment.
How Thoryn selects and calls endpoints
For each DSAR request the RelyingPartyDsarSource:
- Resolves the OAuth
client_ids the subject was known to (recorded consents + authorizations), reusing the hub subject↔client linkage. - Selects every enabled data-return endpoint whose
client_idis in that set (across every environment — Article 15 is not partitioned into test vs live). When the linkage cannot be resolved the source fails open and calls every enabled endpoint (a call for a subject your app never saw is a harmless empty no-op). - Re-validates each
endpointUrlthrough the SSRF guard, signs the request, and calls your endpoint with a bounded timeout.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
400 invalid_endpoint_url on register | The URL is not public HTTPS (private, loopback, or metadata target). Use a public HTTPS URL. |
409 endpoint_already_exists | A data-return endpoint already exists for this client in this environment. Delete it first, or bind to a different environment. |
400 invalid_environment | environmentId does not name an environment of your tenant. |
403 test_mode_not_supported | You called the management API with a test-mode credential. DSAR is platform-live; use a live-mode credential. |
| Your data missing from a bundle | Confirm the endpoint is enabled, its clientId matches an Application the subject actually used, and your endpoint returned 200 with a records array within the timeout. A failing / unreachable endpoint is recorded as an rp_return_error record in the bundle (fail-open) — the bundle still completes. |
Security notes
- The subject key is pseudonymous. Thoryn sends only the platform
sub, never PII. Your endpoint must map it to your own records. - Requests are signed with the tenant ES256 key. Verify the signature — do not trust an unsigned or wrongly-signed request. Treat the endpoint as authenticated server-to-server, not a public API.
- The optional bearer never leaves Vault in the clear. It is envelope-encrypted at rest, decrypted only in memory for the single outbound call, and never returned on a read.
- SSRF is enforced twice — at registration and before every dispatch — and Thoryn refuses redirects, so a 3xx cannot bounce the signed request to another host.
- Fail-open, per endpoint. A failing endpoint never aborts the bundle; it is recorded as an error record so the DSAR still reaches the subject.