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 |
|---|---|---|---|
|
|
|
Submit an erasure/access request (OpenDSR) |
|
|
|
|
Get the status of a request |
|
|
|
|
Cancel a pending request |
|
|
|
|
Verify your access is wired up without submitting a request |
|
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_idplus 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:
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 |
|---|---|---|
|
|
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 |
Provided at onboarding |
|
|
Your application's id: identifies who is calling. |
Provided at onboarding |
|
|
Your application's credential. Keep it secret; never log it or put it in source control. |
Delivered to you separately, through a secure channel |
|
|
Our API's identifier, of the form |
Provided at onboarding |
Request headers
|
Header |
Required |
Meaning |
|---|---|---|
|
|
Yes |
The access token from the call above. |
|
|
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. |
|
|
Yes, on |
Request body is JSON. A |
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):
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:
{
"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 |
|---|---|
|
|
The Data Readiness Hub tenant your call resolved to. Useful confirmation of which tenant you were mapped to when you omit |
|
|
Your own Entra application id, taken from the token. Not secret. |
|
|
Your token satisfied required application roles. |
|
|
The request passed the Front Door origin check. |
|
|
The expiry of the token you presented (its |
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)
{
"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 |
|---|---|---|---|
|
|
Yes |
lowercase UUID v4 |
Consumer-supplied id (OpenDSR). Used in the URL for GET/DELETE. Validated strictly (see below). |
|
|
Yes |
|
|
|
|
Yes |
array, ≥ 1 entry |
See the per-entry fields below. |
|
|
Yes |
RFC 3339 timestamp |
The original data-subject request time (OpenDSR §7.1.1). Anchors |
Each subject_identities entry:
|
Entry field |
Required |
Notes |
|---|---|---|
|
|
Yes |
One of |
|
|
Yes |
The identifier itself (e.g., |
|
|
No |
One of |
|
|
Conditional |
Required when |
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):
"subject_identities": [
{
"identity_type": "source_party_profile_id",
"identity_value": "PP4",
"identity_format": "raw",
"identity_business_unit_code": "EU"
}
]
By individual_id:
"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 freshsubject_request_idand resubmit.
Response
{
"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 aftersubmitted_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 |
|---|---|
|
|
Received, not yet processed. The only state in which the request can still be cancelled. |
|
|
Being processed. |
|
|
Fulfilled. This is the Final status; it will not change again. |
|
|
Cancelled (via |
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
DELETEon a request that isin_progress,completed, or alreadycancelledreturns409, anderror.messagenames 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
202for a cancel that did not take effect. -
A repeated
DELETEon an already-cancelled request is a409(it is no longer pending). UseGETto confirm the status if aDELETEresponse 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:
{ "error": { "code": 403,
"message": "invalid token or tenant not granted",
"correlation_id": "7f3a9c21-4e5b-4a77-9d10-b8c2e1f04a6d" } }
-
error.codeequals the HTTP status code. -
error.messageis a short description of what went wrong (see the table below). -
error.correlation_idis a correlation id unique to that response, also returned in anX-RedPoint-CorrelationIdresponse 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-fielddomain/reason/messagedetail) 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 |
|---|---|
|
|
Malformed body: bad Also, reusing an existing |
|
|
No bearer token: the |
|
|
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 |
|
|
No request with that id for your tenant (GET/DELETE), or an unrecognized path. |
|
|
|
|
|
A |
|
|
The endpoint is temporarily not accepting requests. The response carries a |
|
|
An unexpected internal error. |
Complete example
# 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.