Redpoint CDP™ Documentation
Auto Light Dark
Auto Light Dark

RTBF API reference

Overview

This documents the public right to be forgotten (RTBF) (OpenDSR) request API.

The following table briefly summarizes the available endpoints:

Method

Path

Purpose

Success

POST

/api-op/v1/rtbf/requests

Submit an erasure/access request (OpenDSR)

201 (new request, or an idempotent replay)

GET

/api-op/v1/rtbf/requests/{requestId}

Get the status of a request

200

DELETE

/api-op/v1/rtbf/requests/{requestId}

Cancel a pending request

202 (409 once processing has started; see Cancelling a request)

GET

/api-op/v1/rtbf/self-check

Verify your access is wired up without submitting a request

200

Connecting

Authentication is OAuth2 client credentials, the standard machine-to-machine flow. You need the following: the public base URL, OAuth2 credentials, the DRH tenant id(s), and network eligibility (only in some environments).

  • Base URL: the Redpoint-provided host, of the form https://<env>-<infra-id>-rtbf.redpointcdp.com. All calls go to {BASE}/api-op/v1/rtbf/requests.

  • OAuth2 credentials: Redpoint registers an application for you in its Microsoft Entra tenant and gives you a client_id plus a client secret or certificate, the token endpoint, and the scope to request. You exchange these for a short-lived access token and send it as a bearer token on each call.

  • Your Data Readiness Hub tenant id(s): the account(s) you are authorized to act for (if you act for more than one), also supplied by Redpoint.

  • Permitted egress IPs (only if applicable): if Redpoint's firewall restricts your environment by source IP or region, you supply the outbound IP addresses your integration calls from at onboarding, and Redpoint allow-lists them. Redpoint will tell you at onboarding whether this applies. If it does and your IPs aren't listed, calls are refused before authentication (see How the connection is secured).

Getting a token

Standard client_credentials grant against Microsoft Entra. Any OAuth2 client library, or a plain HTTP POST:

Bash
curl -sS -X POST "https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=<YOUR_CLIENT_ID>" \
  -d "client_secret=<YOUR_CLIENT_SECRET>" \
  -d "scope=<AUDIENCE>/.default"
# -> { "access_token": "eyJ...", "expires_in": 3599, "token_type": "Bearer" }

Cache the token until shortly before expires_in elapses and reuse it across calls; do not mint one per request. Redpoint provides <…> values at onboarding:

Placeholder

What it is

How you get it

<TENANT_ID>

Redpoint's Microsoft Entra directory id. Used only in the token URL, and the same for every Redpoint environment. Not your Data Readiness Hub tenant id, which is sent as X-ClientId (see Request headers).

Provided at onboarding

<YOUR_CLIENT_ID>

Your application's id: identifies who is calling.

Provided at onboarding

<YOUR_CLIENT_SECRET>

Your application's credential. Keep it secret; never log it or put it in source control.

Delivered to you separately, through a secure channel

<AUDIENCE>

Our API's identifier, of the form api://<id>. Identifies which API the token is for. The <id> is a different value from your client id: client_id names your application, scope names ours. Using the same value in both fails the token request.

Provided at onboarding

Request headers

Header

Required

Meaning

Authorization: Bearer <token>

Yes

The access token from the call above.

X-ClientId

Conditional

Which Data Readiness Hub tenant this request is for. Required if your application is authorized for more than one tenant; may be omitted if it is authorized for only one. If you are authorized for several and omit it, the request is rejected.

Content-Type: application/json

Yes, on POST

Request body is JSON. A POST without it is rejected with 415. GET, DELETE, and self-check send no body and don't need it.

You do not send the Front Door origin header; the edge adds it automatically.

X-ClientId is not a credential and doesn’t grant anything on its own; it selects among the tenants your application is authorized for. Naming a tenant you are not authorized for is rejected.

How the connection is secured

The following exists between you and the service and explains behaviors you will see.

  • The public host is an Azure Front Door edge, not the service itself. Front Door terminates Transport Layer Security (TLS), applies a web application firewall and rate limiting, and forwards your request to the service over a private path. The service is not reachable directly (only through this host).

  • Two things must both be true for a call to be served: it arrived through Front Door, and it carried a valid token for an application that Redpoint has authorized for the tenant. A request that fails either test is refused.

Consequences:

  • A request that does not arrive through the approved network path is rejected with 403, the same code as a bad credential, deliberately; the service doesn’t reveal the reason.

  • If Redpoint's firewall restricts by source IP for your environment, your egress IPs must be allow-listed at onboarding, or calls will be refused before being authenticated.

  • There is nothing for you to configure here: you send the headers above, and Front Door adds its own.

Verifying your access (GET /…self-check)

Make this your first call. GET /api-op/v1/rtbf/self-check confirms your credentials and authorization are wired up end to end without submitting a real request. It runs the identical authentication as the request endpoints and, on success, echoes back the context it resolved. It creates no request and no audit record, and does not count as usage of your grant.

Prefer it over the other endpoints for a first connectivity test: POST creates a real, irreversible erasure, and GET/DELETE need a request id that exists, so neither is a safe way to probe.

Send just your bearer token (and X-ClientId only if your application is authorized for more than one tenant):

Bash
curl -sS "https://<env>-<infra-id>-rtbf.redpointcdp.com/api-op/v1/rtbf/self-check" \
  -H "Authorization: Bearer $TOKEN"
# add  -H "X-ClientId: <YOUR_DRH_TENANT>"  only if you are authorized for more than one tenant

On success, it returns 200 with the context it resolved:

JSON
{
  "client_id": "<the Data Readiness Hub tenant you were resolved to>",
  "app_id": "<your Entra application id>",
  "roles_satisfied": true,
  "origin_verified": true,
  "token_expires_at": "2026-09-09T22:49:24.000Z"
}

Field

Meaning

client_id

The Data Readiness Hub tenant your call resolved to. Useful confirmation of which tenant you were mapped to when you omit X-ClientId.

app_id

Your own Entra application id, taken from the token. Not secret.

roles_satisfied

Your token satisfied required application roles.

origin_verified

The request passed the Front Door origin check.

token_expires_at

The expiry of the token you presented (its exp).

The success body contains only the fields above, no deployment configuration of any kind. On failure, self-check returns the same 401 / 403 / 503 (the §7.6 error object with a correlation id) as the request endpoints. The Status codes & errors section below, including the correlation-id workflow, applies here unchanged. A 200 means your access is fully wired up and you can proceed to submit requests.

Submitting a request (POST)

JSON
{
  "subject_request_id": "a7551968-d5d6-44b2-9831-815ac9017798",
  "subject_request_type": "erasure",
  "subject_identities": [
    { "identity_type": "email", "identity_value": "user@example.com", "identity_format": "raw" }
  ],
  "submitted_time": "2026-07-17T12:00:00Z"
}

Field

Required

Type / allowed values

Notes

subject_request_id

Yes

lowercase UUID v4

Consumer-supplied id (OpenDSR). Used in the URL for GET/DELETE. Validated strictly (see below).

subject_request_type

Yes

erasure | access

erasure: Finds records matching the identifiers you send and removes them. Depending on your configuration, the data is either deleted (purged) or masked so no personal data remains.

access: Accepts and tracks a request to report what data is held about the subject. Results are delivered as CSV files via SFTP.

subject_identities

Yes

array, ≥ 1 entry

See the per-entry fields below.

submitted_time

Yes

RFC 3339 timestamp

The original data-subject request time (OpenDSR §7.1.1). Anchors expected_completion_time. Missing/malformed → 400.

Each subject_identities entry:

Entry field

Required

Notes

identity_type

Yes

One of email | source_party_profile_id | individual_id. Any other value → 400.

identity_value

Yes

The identifier itself (e.g., user@example.com, PP1). Blank/whitespace → 400.

identity_format

No

One of raw | sha1 | md5 | sha256; defaults to raw when omitted. A format the current deployment cannot match is rejected with 400 (deployments currently match raw only).

identity_business_unit_code

Conditional

Required when identity_type is source_party_profile_id (that id is only unique on value + business unit); optional otherwise. Part of the identity fingerprint; the same value under a different business unit is a different subject.

Both enum fields accept any casing and are canonicalized to lowercase in storage/responses; identity_business_unit_code is stored verbatim.

The subject can be identified in any of three identity_type spaces (the example above uses email). identity_business_unit_code is required only for source_party_profile_id; that identifier is unique only within a business unit, and is not needed for email or individual_id.

By source_party_profile_id (business-unit code required):

JSON
"subject_identities": [
  {
    "identity_type": "source_party_profile_id",
    "identity_value": "PP4",
    "identity_format": "raw",
    "identity_business_unit_code": "EU"
  }
]

By individual_id:

JSON
"subject_identities": [
  {
    "identity_type": "individual_id",
    "identity_value": "100000000076",
    "identity_format": "raw"
  }
]

Identity semantics: OR / union. Multiple identities in one request are OR'd: all records matching any supplied identifier are erased (not the intersection). Two consequences to understand:

  • All identities in one request must belong to a single data subject. Redpoint does not validate identities in this context; it relies on your assertions.

  • A shared identifier erases all profiles bearing it (e.g., an email on two profiles removes both).

subject_request_id rules

Must be a lowercase UUID version 4, validated against:

^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$

The version digit must be 4 and the variant digit one of 8/9/a/b; uppercase is rejected with 400. Generate one with python3 -c "import uuid; print(uuid.uuid4())" or uuidgen | tr '[:upper:]' '[:lower:]'.

Idempotency (safe to retry POST)

subject_request_id is the idempotency key per tenant:

  • Same id + identical request → 201, returning the existing request unchanged, indistinguishable from the original submission. That is the point of an idempotency key: nothing new is created; the response is what a dropped-response retry needs.

  • Same id + materially different request (different type/identities/submitted_time) → 400. This error indicates that the id has been reused for a different subject; mint a fresh subject_request_id and resubmit.

Response

JSON
{
  "subject_request_id": "a7551968-d5d6-44b2-9831-815ac9017798",
  "subject_request_type": "erasure",
  "request_status": "pending",
  "received_time": "2026-07-17T12:00:03.000Z",
  "expected_completion_time": "2026-08-16T12:00:00.000Z"
}
  • New requests start at request_status: "pending".

  • expected_completion_time = 30 days after submitted_time (the statutory clock runs from the subject's original request, not from receipt). This is computed once and not recalculated.

subject_identities is not echoed back, on any response. A status poll happens most while the request is still pending/in_progress, when the identity values are still plaintext, so returning them would reference the data subject's PII on every poll.

Status values

Value

Meaning

pending

Received, not yet processed. The only state in which the request can still be cancelled.

in_progress

Being processed.

completed

Fulfilled. This is the Final status; it will not change again.

cancelled

Cancelled (via DELETE while status is still pending). Final.

There is no failed status: a request that could not be completed on one pass is left in_progress and retried on the next scheduled run, rather than surfaced as a terminal failure.

Cancelling a request (DELETE)

DELETE /api-op/v1/rtbf/requests/{requestId} cancels a request that is still pending (OpenDSR §9). It returns 202 Accepted with the request body showing request_status: "cancelled"; the record remains readable via GET.

  • Once processing has started, the request can no longer be cancelled; an erasure that is underway cannot be un-done. A DELETE on a request that is in_progress, completed, or already cancelled returns 409, and error.message names the current status, e.g., request cannot be cancelled in status: in_progress. The request is left as it was.

  • The rule is enforced in the database write; if your cancel arrives at the same moment processing begins, the response tells you which won. You will not get a 202 for a cancel that did not take effect.

  • A repeated DELETE on an already-cancelled request is a 409 (it is no longer pending). Use GET to confirm the status if a DELETE response was lost.

Deviation from OpenDSR §7.5. The spec asks for a blanket 400 on errors. This endpoint returns 409 for the not-cancellable case, because it also returns the reason (the request's current status), and a generic connector's error branch handles either code. The body is still the §7.6 error object.

Status codes & errors

Each error from these endpoints, including ones raised by the underlying framework before your request reaches application logic (a malformed body, wrong Content-Type, an unknown path), returns the OpenDSR §7.6 error object:

JSON
{ "error": { "code": 403,
             "message": "invalid token or tenant not granted",
             "correlation_id": "7f3a9c21-4e5b-4a77-9d10-b8c2e1f04a6d" } }
  • error.code equals the HTTP status code.

  • error.message is a short description of what went wrong (see the table below).

  • error.correlation_id is a correlation id unique to that response, also returned in an X-RedPoint-CorrelationId response header (the two match). It is a Redpoint extension to the §7.6 object; it’s additive, so you may ignore it in normal operation. Quote it when you contact Redpoint about a failure. See the note below.

  • The OPTIONAL errors[] array (per-field domain/reason/message detail) is not currently populated.

Getting help with a rejected request: quote the correlation id.

Several different problems produce the same 403. This is deliberate; the response cannot tell you which, because doing so would let anyone probe for which applications and customer accounts exist. The correlation id is how we bridge that. Each error you receive has a correlation id; it is the key to a server-side log entry that records the cause of the error.

When a call fails that you cannot resolve from the table below, send Redpoint support the error.correlation_id value (or the X-RedPoint-CorrelationId header), along with the approximate time and the endpoint you called. We can then tell you what went wrong, for example that the token was issued for the wrong audience, or that your authorization for that tenant has been disabled, without the API disclosing it.

Two practical notes:

  • Report it promptly: the underlying log entries are retained for a limited window (your Redpoint contact can confirm the retention period for your environment).

  • Log the correlation id on your side: log non-2xx responses, so they’re available later. The correlation id is random and carries no information; it is only useful to Redpoint.

Code

When

400

Malformed body: bad subject_request_id, missing/invalid subject_request_type, empty subject_identities, invalid identity_type/identity_format, unsupported format, source_party_profile_id without identity_business_unit_code, missing/malformed submitted_time, a body that isn't valid JSON.

Also, reusing an existing subject_request_id with a materially different request.

401

No bearer token: the Authorization header is missing, blank, or uses a different scheme. Fix the header and retry.

403

The credential was not accepted. Deliberately one response for each cause, so it reveals nothing about which applications or tenants exist: the token is invalid, expired, or issued for the wrong audience; your application is not authorized for the requested tenant, or that authorization has been disabled; you are authorized for several tenants and did not send X-ClientId; or the request did not arrive through the approved network path. Check token validity and audience first, then confirm with Redpoint that your tenant authorization is active: quoting error.correlation_id lets us identify the exact cause.

404

No request with that id for your tenant (GET/DELETE), or an unrecognized path.

409

DELETE on a request that is no longer pending (in_progress, completed, or already cancelled). error.message names the current status. The request is unchanged. Do not retry the cancel; poll GET for the outcome instead.

415

A POST whose Content-Type is not application/json (e.g., form-encoded or plain text). Set the header and retry.

503

The endpoint is temporarily not accepting requests. The response carries a Retry-After header (seconds). Back off and retry rather than treating this as a hard failure; your statutory deadline is anchored on submitted_time, not on when we accept the call.

500

An unexpected internal error. error.message is a stable generic string, not exception detail. Quote error.correlation_id, which lets us find the underlying fault.

Complete example

Bash
# 1. Get a token (cache it until shortly before it expires)
TOKEN=$(curl -sS -X POST "https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=<YOUR_CLIENT_ID>" \
  -d "client_secret=<YOUR_CLIENT_SECRET>" \
  -d "scope=<AUDIENCE>/.default" | jq -r .access_token)

# 2. Verify access first (creates nothing)
curl -sS "https://<env>-<infra-id>-rtbf.redpointcdp.com/api-op/v1/rtbf/self-check" \
  -H "Authorization: Bearer $TOKEN"
# -> 200 { "client_id": "...", "app_id": "...", "roles_satisfied": true, "origin_verified": true, "token_expires_at": "..." }

# 3. Submit the request
curl -sS -X POST "https://<env>-<infra-id>-rtbf.redpointcdp.com/api-op/v1/rtbf/requests" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-ClientId: <YOUR_DRH_TENANT>" \
  -H "Content-Type: application/json" \
  -d '{
        "subject_request_id": "a7551968-d5d6-44b2-9831-815ac9017798",
        "subject_request_type": "erasure",
        "subject_identities": [
          { "identity_type": "email", "identity_value": "user@example.com", "identity_format": "raw" }
        ],
        "submitted_time": "2026-07-17T12:00:00Z"
      }'
# -> 201 Created, Location: .../api-op/v1/rtbf/requests/{requestId}

# 4. Cancel it while still pending
curl -sS -X DELETE "https://<env>-<infra-id>-rtbf.redpointcdp.com/api-op/v1/rtbf/requests/a7551968-d5d6-44b2-9831-815ac9017798" \
  -H "Authorization: Bearer $TOKEN" -H "X-ClientId: <YOUR_DRH_TENANT>"
# -> 202 Accepted, request_status: "cancelled"   (or 409 if processing had already started)

# If a call fails, capture the correlation id before doing anything else:
#   curl -sS -i ... | grep -i '^X-RedPoint-CorrelationId:'   (or read error.correlation_id from the body)

To certify a connector against the test environment, follow the RTBF onboarding guide.

Last updated: