/ Docs

Technical Implementation Guide

Last updated August 28, 2026 · View as Markdown

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/create

Creates 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/presign

Call 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/import

Same 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:

  • jurisdiction requires all three keys present (region, country, state) - null values are fine, but omitting the region key 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

  1. POST /api/dsr/v2/attachment/presign → get an upload URL + token.
  2. PUT the authorization document (e.g., a signed power-of-attorney PDF) directly to that URL.
  3. POST /api/dsr/v2/create with submissionType: "AUTHORIZED_AGENT_SUBMISSION" and a customFields entry of type CUSTOM_DSR_FIELD_TYPE_ATTACHMENT referencing 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

  1. Find the vendor's tile in the Integrations catalog and click into it.
  2. Click Add Connection, name it, and save.
  3. 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).
  4. 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).
  5. 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)

  1. Go to Settings/Configuration → Integrations and select the Custom DSR Task tile.
  2. Click Add Connection. Give it a name that identifies what it points to (e.g., Internal Billing System - DSR).
  3. 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.
  4. 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)

  1. Go to Settings/Configuration → Integrations and select the Custom DSR Trigger Task tile.
  2. Click Add Connection and name it.
  3. 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.
  4. 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:

  1. Add Step → Data Access Webhook (for the access/export side of the request).
  2. 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_url reference 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.
  3. 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.
  4. 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:

  1. On the connection - set workflow_id to the workflow that should process incoming requests from this trigger (as described above).
  2. 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):

  • stepTimeout sets how long the step waits before its deadline passes and it's marked failed with "Step has timed out."
  • On a failure, retryAfter sets how long the platform waits before automatically retrying, and each retry decrements a remaining-attempts counter seeded from retryAttempts.
  • Once retries are exhausted, onNoMoreRetries fires (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 onCallback evaluator runs against every callback payload received. Returning the STEP.PENDING action from an evaluator (for example, when the payload's status field 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 (and pathToResourceName/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 setting staticSourceName for 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 - stepTimeout is 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 - for onStart - 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) - commonly FALSE (treat as non-match) or RAISE.
  • ifTrue / ifFalse - the Action to take depending on the condition's result. Either can be omitted if there's nothing to do on that branch.
  • action (on an Action) - the outcome to trigger, e.g. STEP.END_STEP.FAILED, STEP.END_STEP.SUCCESS, STEP.END_STEP.SKIPPED, or STEP.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.