Technical Implementation Guide
Audience: Engineers integrating with the DSR API, connecting third-party systems, or building custom fulfillment integrations
This guide covers the DSR API, connecting pre-built vendor integrations, and how to build a custom integration (Custom DSR Task / Custom DSR Trigger Task) against a system that doesn't have a pre-built connector.
The DSR API
Authentication
Every request is authenticated via HMAC-SHA256 request signing - there is no bearer token. Sign the raw request body with your API key and send the result as the Authorization header:
Authorization: Relyance1 {hex(hmac_sha256(api_key, raw_request_body))}import hashlib
import hmac
import json
import requests
body = json.dumps(payload, separators=(",", ":")) # sign the exact bytes you send
signature = hmac.new(api_key.encode(), body.encode(), hashlib.sha256).hexdigest()
response = requests.post(
"https://api.relyance.ai/api/dsr/v2/create",
data=body, # send the same bytes you signed - do not re-serialize
headers={
"Authorization": f"Relyance1 {signature}",
"Content-Type": "application/json",
},
)Common pitfall: signing one JSON serialization and sending another (e.g., signing a dict then letting an HTTP client re-serialize it with different key ordering or whitespace) is the most common cause of a 401. Sign the exact byte string you transmit.
Endpoints
Create a DSR
POST /api/dsr/v2/createCreates a new DSR. Minimal example:
{
"type": "DSAR_TYPE_ACCESS_DATA",
"submissionType": "DATA_SUBJECT_SUBMISSION",
"dataSubject": {
"type": "Customer",
"email": "jane.doe@example.com",
"firstName": "Jane",
"lastName": "Doe",
"jurisdiction": {
"region": "US",
"country": "United States",
"state": "California"
}
}
}| Response | Meaning |
|---|---|
201 |
DSR created; response body includes the new DSR's id |
400 |
Schema validation failure - see Errors below |
401 |
Signature/key issue - see Authentication above |
Get a pre-signed attachment upload URL
POST /api/dsr/v2/attachment/presignCall this before create whenever the request needs a file attached (most commonly, an authorized-agent authorization document). Returns a pre-signed upload URL and a token you'll reference in the subsequent create call.
Bulk-import DSRs
POST /api/dsr/admin/importSame authentication and dataSubject/jurisdiction schema as create, but accepts a batch - useful for migrating historical requests from a prior system rather than replaying them one at a time.
Request schema reference
| Field | Type | Notes |
|---|---|---|
dataSubject.type |
string | Data subject category (e.g., Customer, Employee) |
dataSubject.email / emails |
string / array | At least one is typically required |
dataSubject.firstName / lastName |
string | |
dataSubject.userID |
string | |
dataSubject.phoneNumbers |
array | |
dataSubject.customFields |
array | Submission-form custom field answers |
dataSubject.jurisdiction.region |
string | Required key - must be present even if null |
dataSubject.jurisdiction.country |
string | nullable |
dataSubject.jurisdiction.state |
string | nullable |
submissionType |
enum | DATA_SUBJECT_SUBMISSION (default) or AUTHORIZED_AGENT_SUBMISSION |
businessEntityId |
string | Required only if your tenant runs Multi-Portal - see Admin Configuration Guide |
Two schema behaviors to design around:
jurisdictionrequires all three keys present (region,country,state) -nullvalues are fine, but omitting theregionkey entirely returns a 400.- Unknown fields are rejected outright (
extra='forbid') - a typo'd or deprecated field name hard-fails the request rather than being silently dropped. Validate against the current schema rather than reusing an older integration's field list.
Errors
| Status | Cause |
|---|---|
401 |
Bad or missing signature, wrong API key |
400 |
Missing required jurisdiction keys, unknown fields, missing businessEntityId under Multi-Portal, malformed customFields |
Recipe: submitting on behalf of an authorized agent
POST /api/dsr/v2/attachment/presign→ get an upload URL + token.PUTthe authorization document (e.g., a signed power-of-attorney PDF) directly to that URL.POST /api/dsr/v2/createwithsubmissionType: "AUTHORIZED_AGENT_SUBMISSION"and acustomFieldsentry of typeCUSTOM_DSR_FIELD_TYPE_ATTACHMENTreferencing the upload token, mapped to the workflow's authorized-agent-document field:
{
"type": "DSAR_TYPE_ACCESS_DATA",
"submissionType": "AUTHORIZED_AGENT_SUBMISSION",
"dataSubject": { "...": "as above" },
"customFields": [
{
"type": "CUSTOM_DSR_FIELD_TYPE_ATTACHMENT",
"fieldMapping": "AUTHORIZED_AGENT_DOCUMENTS",
"value": "{{presign_token_from_step_1}}"
}
]
}This three-call pattern is proven in production for spreadsheet-driven intake pipelines (e.g., a Google Sheets + Apps Script front end that collects agent name/email/document token per row and calls the API per row) - a useful reference architecture if your own intake isn't going through the Privacy Portal directly.
Pre-built connectors
Before building a custom integration, check whether Relyance already ships a pre-built connector for the system you need - it's the faster path and doesn't require you to know the target system's API surface yourself. A pre-built connector already knows how to search for a data subject's records in that system and either return them or delete them, wherever the system's own API supports it.
Where to find them
Pre-built connectors live in the same place as every other Relyance integration: Settings/Configuration → Integrations. Browse or search the catalog for the system you want to connect - if a tile exists for it, a pre-built DSR connector is available (or can be, once DSR processing is enabled on it - see below).
Setting one up
- Find the vendor's tile in the Integrations catalog and click into it.
- Click Add Connection, name it, and save.
- On the connection detail page, choose one of the vendor's supported authentication types and fill in its fields. Depending on the vendor this may be an API token/app token, a JWT, a fully custom vendor-defined form, or two-legged OAuth (client-credentials style, no user redirect - typically the recommended option where a vendor offers it).
- Enable DSR processing on the connection - most vendor connections expose a Process DSRs toggle once base authentication succeeds; turning it on grants the additional scopes/permissions the connector needs to fetch or delete subject data (some vendors need broader API scope for deletion than for read-only access, so this may prompt you to re-authenticate with expanded permissions).
- Save. Once active with DSR processing enabled, the connection becomes selectable in any workflow step that supports it.
Using it in a workflow
In the workflow's Steps tab, add a Third Party Access step (to fetch data) or a Third Party Delete step (to delete data) and select your connection from the dropdown. Unlike a Data Access/Delete Webhook step, there's no URL, method, headers, or body to configure - the connector already knows how to search and retrieve/delete records in that system; you're just pointing the step at an authenticated connection.
If a vendor tile isn't in the catalog, or the system is a plain database your team manages directly, see Custom DSR Task and Custom DSR Trigger Task below for how to connect it yourself.
Custom DSR Task and Custom DSR Trigger Task
These are the two building blocks for connecting a system that doesn't have a pre-built connector. They solve opposite problems:
| Direction | Answers | Key config field | |
|---|---|---|---|
| Custom DSR Task | Outbound - Relyance calls the target system | "How do we fetch/delete data in system X?" | dsr_access_url / dsr_delete_url |
| Custom DSR Trigger Task | Inbound - the target system calls Relyance | "How does system X tell Relyance a new request exists?" | workflow_id |
Both are configured as connections under a distinct integration tile in the catalog, and both share the same three authentication options (below). A connection only holds credentials + endpoint metadata - the actual request shape (URL, method, headers, body) for a Custom DSR Task is defined per-step in the workflow editor, not on the connection itself. This split means rotating a credential never requires touching every step/workflow that references it.
Setting up a Custom DSR Task connection (credentials)
- Go to Settings/Configuration → Integrations and select the Custom DSR Task tile.
- Click Add Connection. Give it a name that identifies what it points to (e.g.,
Internal Billing System - DSR). - On the connection detail page, configure:
- Authentication - one of the three types below.
dsr_access_url/dsr_delete_url(optional) - reference metadata for your own team's documentation; these are not called at runtime. The URL actually invoked at runtime is set independently on each workflow step (see below) - a common point of confusion, since it's easy to assume setting this field is sufficient on its own.- Vendor association - associate the connection with either a catalogued vendor (feeds external RoPA/vendor mapping) or a named internal service (feeds internal RoPA generation). Set one or the other, not both.
- Save. The connection must show active (i.e., its credentials verify successfully) before it will appear as selectable in a workflow step.
Setting up a Custom DSR Trigger Task connection (credentials)
- Go to Settings/Configuration → Integrations and select the Custom DSR Trigger Task tile.
- Click Add Connection and name it.
- Configure the same Authentication options as above, plus the required
workflow_id- the workflow this connection will feed incoming requests into. Every request that arrives through this trigger is routed to this one workflow; if you need to trigger multiple workflows from the same external system, create one Trigger Task connection per target workflow. - Save. Relyance will expose an inbound endpoint/credential pair for the external system to call - consult your connection detail page for the exact inbound URL and expected payload shape once the connection is active.
Authentication options (shared by both connection types)
Pick based on what the target system supports:
1. Custom API Integration - a generic JSON credential blob. Set type to select the scheme:
// API Key
{ "type": "apikey", "api_key": "..." } // sent as X-API-Key header
// Bearer token
{ "type": "bearer", "token": "..." } // sent as Authorization: Bearer <token>
// HTTP Basic
{ "type": "basic", "username": "...", "password": "..." }
// HTTP Digest
{ "type": "digest", "username": "...", "password": "..." }
// OAuth2 (client credentials, specified inline rather than via the dedicated form)
{ "type": "oauth2", "client_id": "...", "client_secret": "...", "token_url": "..." }
// No auth
{ "type": "none" }2. OAuth2 (Client Credentials) - a dedicated form (use this instead of the inline oauth2 blob above when you don't need anything custom): token_url, client_id, client_secret, and a toggle for whether credentials are sent as HTTP Basic auth on the token request or in the POST body. Tokens are cached and refreshed automatically before they expire - you don't need to handle token refresh yourself.
3. OAuth2 (Authorization Code) - full three-legged OAuth: auth_url, token_url, client_id, client_secret, and a scopes list, with optional PKCE support. Use this when the target system requires a user-in-the-loop consent grant rather than a machine-to-machine credential.
Wiring a Custom DSR Task into a workflow
Once the connection is active, it's available to any workflow. In the workflow's Steps tab:
- Add Step → Data Access Webhook (for the access/export side of the request).
- In the step editor:
- Connection - select your Custom DSR Task connection from the dropdown (only active, DSR-capable connections are listed). Use the inline Configure/Edit Connection link if you need to jump back and fix credentials.
- URL - the actual endpoint called when this step runs. Independent of the connection's
dsr_access_urlreference field. - Request Method - GET/POST/PUT/PATCH/DELETE.
- Custom Headers - any headers your endpoint needs beyond what the connection's auth already injects (e.g., a tenant-identifying header,
Content-Type). - Body - the request payload, including any templated/dynamic fields from the subject's data.
- Advanced Config - the raw JSON editor for anything not covered by the structured fields (response-path extraction, conditional skip logic, and more) - see Advanced Config (Step JSON) Reference below.
- Add Step → Data Delete Webhook and repeat, pointing at the system's deletion endpoint. You can reuse the same connection if access and delete share credentials, or select a different one if they don't.
- Save the workflow.
At runtime, each step authenticates using its selected connection's credentials, then sends the request exactly as configured on that step.
Wiring a Custom DSR Trigger Task
A Trigger Task connection doesn't get added as a workflow step - but it does require configuration on both the connection and the target workflow:
- On the connection - set
workflow_idto the workflow that should process incoming requests from this trigger (as described above). - On the workflow - open the target workflow's Overview tab and add an entry under Triggers. Select the Custom DSR Trigger Task connection you created, then configure its Rules (a JSON editor) - this is what determines how an incoming payload is matched/mapped before the workflow accepts it as a new DSR. A workflow won't actually start processing requests from the connection until this Trigger entry exists on the workflow side, even though the connection already points at the workflow via
workflow_id.
Once both sides are configured, the external system calls Relyance's inbound endpoint whenever it needs to start a request, authenticating with the credentials configured on the connection. If the external system's payload doesn't map cleanly to Relyance's expected request shape, use the Rules JSON to bridge the difference, or add a transformation layer (middleware) on the external system's side before it calls Relyance.
Example Rules configuration - mapping an inbound ticketing-system payload to a new Access DSR for a Customer in the US:
{
"match": {
"requestType": "data_access"
},
"map": {
"type": "DSAR_TYPE_ACCESS_DATA",
"dataSubject.type": "Customer",
"dataSubject.email": "$.payload.customer_email",
"dataSubject.firstName": "$.payload.customer_first_name",
"dataSubject.lastName": "$.payload.customer_last_name",
"dataSubject.jurisdiction.region": "$.payload.region",
"dataSubject.jurisdiction.country": "$.payload.country",
"dataSubject.jurisdiction.state": "$.payload.state"
}
}Treat this as illustrative rather than a literal schema reference - the exact fields your Rules JSON can reference depend on the shape of the payload your external system actually sends, and the Rules editor's field picker will reflect what's available for your specific connection. The general pattern holds regardless: a match condition decides whether an inbound payload should create a DSR at all, and a map decides how its fields populate the resulting request's dataSubject/jurisdiction (the same schema used by the DSR API).
Advanced Config (Step JSON) Reference
Every workflow step type exposes an Advanced Config control ("Edit JSON") in its editor, opening a raw JSON editor bound to the step's rule configuration (ruleConfig over GraphQL; rule_config on the backend StepConfig model). There's no schema enforced in the editor itself - it's a plain string field - so this reference documents the real, current schema (RuleConfigV2) so you know exactly what you're editing. When you open a brand-new step, the platform pre-populates this field with a server-generated default for that step type rather than an empty object.
Top-level fields
| JSON key | Type | Default | Controls |
|---|---|---|---|
version |
int | 2 |
Schema version |
stepTimeout |
int (seconds) | 300 |
How long the step waits before it's considered timed out and marked ERROR |
retryAfter |
int (seconds) | 3600 |
Delay before an automatic retry fires after a failure |
retryAttempts |
int | 3 |
Number of retries before the step gives up and fires onNoMoreRetries |
asyncCallback |
bool | false |
Marks the step as expecting an asynchronous webhook callback rather than completing on the initial response - see Async / callback steps |
pathToSubjectData |
JMESPath string | "body" |
Where in the step's synchronous API response the subject-data payload lives |
pathToCallbackData |
JMESPath string | "data" |
Where in an async callback payload the subject-data payload lives |
pathToSourceName |
JMESPath string | "source_name" |
Where in a response/callback to find a value used as that result's "Source" label |
pathToResourceName |
JMESPath string | "resource_name" |
Where in a response/callback to find a value used as that result's "Resource" label |
staticSourceName |
string | null |
null |
Hard-coded Source label, overriding pathToSourceName |
staticResourceName |
string | null |
null |
Hard-coded Resource label, overriding pathToResourceName |
subjectMap |
object | null |
null |
Key/value remapping of subject fields based on values found in the response |
onStart |
array of ResponseEvaluator | [] |
Checked before the step runs - typically used to skip based on an earlier step's outcome |
onRequest |
array of ResponseEvaluator | vendor default (status-code checkpoints) | Checked against the synchronous API response |
onCallback |
array of ResponseEvaluator | vendor default | Checked against each async callback payload |
onTimeout / onSuccess / onFailed / onError / onSkipped / onCompleted / onNoMoreRetries |
array of ResponseEvaluator | [] |
Fire when the step reaches that respective outcome |
Retry and timeout behavior
Retry count and timeout are genuinely configurable per step through this JSON - retryAttempts, retryAfter, and stepTimeout directly override the step's runtime behavior (they aren't just cosmetic):
stepTimeoutsets how long the step waits before its deadline passes and it's marked failed with "Step has timed out."- On a failure,
retryAftersets how long the platform waits before automatically retrying, and each retry decrements a remaining-attempts counter seeded fromretryAttempts. - Once retries are exhausted,
onNoMoreRetriesfires (commonly used to end the step as failed and/or notify someone).
Every step type also has a system-level default retry count and timeout baked into its step type definition (for example, a Data Access Webhook step defaults to 3 retries / 300s, while a Third Party Access step defaults to 1 retry / 3600s, and an interactive/manually-triggered email step defaults to a 30-day timeout). Those per-type defaults are what you're overriding when you set retryAttempts/retryAfter/stepTimeout explicitly in Advanced Config - if you omit them, the step type's own default applies.
Async / callback steps
Set "asyncCallback": true on any step that doesn't get its result back in the same request/response cycle - for example, a third-party system that receives your request and then calls back later (possibly more than once) as it processes it. With this set:
- The step enters a waiting state (surfaced as Waiting for Third Party in the request list) rather than completing after the initial response.
- Each
onCallbackevaluator runs against every callback payload received. Returning theSTEP.PENDINGaction from an evaluator (for example, when the payload'sstatusfield reads"PENDING") keeps the step waiting for further callbacks instead of ending it - this is how a step can process several distinct payloads over time (e.g., a document upload followed later by a verification result) without you needing to declare a fixed count up front. - Each callback payload is tracked as its own distinct result, labeled using whatever
pathToSourceName/staticSourceName(andpathToResourceName/staticResourceName) resolve to for that payload. This is what the step editor's "Response Name" field is setting under the hood - giving each expected callback a static, human-readable Response Name is equivalent to settingstaticSourceNamefor that payload, and is what later steps/templates reference via{{ STEPS.ID.responses.<response_name>.<field> }}(see Admin Configuration Guide). - There is no separate "expected callback count" or overall async timeout field -
stepTimeoutis the only timeout, and it's re-armed on each retry/attempt rather than tracked separately per callback.
The evaluator, condition, and action objects
onStart, onRequest, onCallback, and the outcome hooks (onSuccess, onFailed, etc.) are all arrays of the same shape:
{
"condition": { "target": "status_code", "operator": "EQUAL", "value": 400, "valueType": "ANY", "onError": "FALSE", "caseSensitive": false },
"ifTrue": { "action": "STEP.END_STEP.FAILED", "message": "API Failure: 400 Bad Request", "payload": {}, "origin": "WORKFLOW" },
"ifFalse": { "action": "STEP.END_STEP.SUCCESS", "message": "", "payload": {}, "origin": "WORKFLOW" }
}condition.target- a JMESPath expression evaluated against the current context (the API response, the callback payload, or - foronStart- an upstream step's context, referenced as<STEP_ID>_CONTEXT.<field>, e.g."STEP_1_CONTEXT.stepState").condition.operator- a comparator, e.g.EQUAL,HAS_KEYS.condition.value/valueType- what to compare against, and how to cast it.condition.onError- what to do if evaluating the target itself errors (e.g., the path doesn't exist) - commonlyFALSE(treat as non-match) orRAISE.ifTrue/ifFalse- theActionto take depending on the condition's result. Either can be omitted if there's nothing to do on that branch.action(on anAction) - the outcome to trigger, e.g.STEP.END_STEP.FAILED,STEP.END_STEP.SUCCESS,STEP.END_STEP.SKIPPED, orSTEP.PENDING(keep waiting - see Async / callback steps above).message- human-readable text that shows up in the audit log for that outcome.
Full real example (Rule Webhook step)
{
"version": 2,
"stepTimeout": 300,
"retryAfter": 3600,
"retryAttempts": 3,
"asyncCallback": true,
"pathToSubjectData": "body",
"pathToCallbackData": "data",
"pathToSourceName": "source_name",
"pathToResourceName": "resource_name",
"onStart": [],
"onRequest": [
{
"condition": { "target": "status_code", "operator": "EQUAL", "value": 400, "valueType": "ANY", "onError": "FALSE", "caseSensitive": false },
"ifTrue": { "action": "STEP.END_STEP.FAILED", "message": "API Failure: 400 Bad Request", "payload": {}, "origin": "WORKFLOW" }
},
{
"condition": { "target": "status_code", "operator": "EQUAL", "value": 404, "valueType": "ANY", "onError": "FALSE", "caseSensitive": false },
"ifTrue": { "action": "STEP.END_STEP.SKIPPED", "message": "No record found for this subject", "payload": {}, "origin": "WORKFLOW" }
}
],
"onCallback": [
{
"condition": { "target": "@", "operator": "HAS_KEYS", "value": ["data", "status"], "valueType": "ANY", "onError": "RAISE", "caseSensitive": false },
"ifFalse": { "action": "STEP.END_STEP.FAILED", "message": "Payload missing required key(s) ['data', 'status']", "payload": {}, "origin": "WORKFLOW" }
},
{
"condition": { "target": "status", "operator": "EQUAL", "value": "PENDING", "valueType": "ANY", "onError": "RAISE", "caseSensitive": false },
"ifTrue": { "action": "STEP.PENDING", "message": "Expecting more data from service", "payload": {}, "origin": "WORKFLOW" }
}
],
"onTimeout": [], "onSuccess": [], "onFailed": [], "onError": [], "onSkipped": [], "onCompleted": [], "onNoMoreRetries": []
}An onStart condition referencing an upstream step looks like:
{
"condition": { "target": "STEP_1_CONTEXT.stepState", "operator": "EQUAL", "value": "SKIPPED", "valueType": "ANY", "onError": "FALSE", "caseSensitive": false },
"ifTrue": { "action": "STEP.END_STEP.SKIPPED", "message": "Previous step skipped. Skipping this step.", "payload": {}, "origin": "WORKFLOW" }
}After editing Advanced Config, use Test Step and check the resulting step-testing audit log to confirm the JSON did what you expected before publishing the workflow.
Troubleshooting integration connections
- Sudden 401/403 errors from a previously-working connection are almost always expired or rotated credentials on the target system's side (API key rotated, OAuth app re-authorized, service account disabled). Check the connection's auth configuration first before assuming a Relyance-side issue - the audit log for the affected step will show the exact HTTP status and response body returned by the target system (see Audit Logs, Troubleshooting & Reporting).
- A field is missing from an Access response even though it's visible in the target system's UI - not every field surfaced in a vendor's own UI is exposed through its API. This is a limitation of the target system, not Relyance; confirm with the vendor whether the field is available via API at all before assuming a configuration error.