API reference

Redacted API

The Redacted API lets you run data-broker exposure scans, file verified removals, and read removal status programmatically — the same operations the dashboard and our MCP agents use. It is organized around predictable, resource-oriented URLs, accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs, status codes, and authentication.

Base URL

https://app.redacted.example/api/v1

All requests must be made over HTTPS and authenticated with a Bearer token. Every endpoint is tenant-scoped and enforced at the database layer — a credential can only ever read or act on the subject it was issued for. Personal data (PII) is never returned in list or detail responses; evidence artifacts are fetched separately through short-lived signed URLs.

Don't have a key yet? Talk to us about API access →

A request
curl https://app.redacted.example/api/v1/status \
  -H "Authorization: Bearer rdk_8f3c2a1b9d4e6f70_4d9f8e7a6b5c..."
200 OKResponse
{
  "exposureCount": 38,
  "runningCases": 4,
  "verifiedRemovedCount": 12,
  "blockers": [],
  "statusBreakdown": {
    "found": 22,
    "request_sent": 4,
    "verified_removed": 12
  },
  "nextScanAt": "2026-07-01T09:00:00.000Z"
}

Authentication

The API authenticates every request with a Bearer token in the Authorization header. A missing or invalid credential returns 401 Unauthorized — there is no unauthenticated access to any resource.

API keys

Partner API keys look like rdk_<keyId>_<secret>. The keyId is a 16-byte hex identifier and the secret is a 32-byte hex value. Only a SHA-256 hash of the secret is stored — the full token is shown exactly once when the key is created, so store it somewhere safe. Treat it like a password: it carries the scopes it was granted and can act on your tenant's data.

Other credential types

Two additional Bearer formats are accepted on the same header. JWKS-verified OAuth JWTs are used by the MCP server and external agents (Claude, ChatGPT); the token's scopes are intersected with the underlying grant. The web dashboard uses an Auth.js session cookie instead of a Bearer token. For server-to-server integrations, use an API key.

Never embed an API key in client-side code or a public repository. Keys belong on your server, loaded from an environment variable or secret manager.

Authenticated request
curl https://app.redacted.example/api/v1/exposures \
  -H "Authorization: Bearer $REDACTED_API_KEY"
Missing / invalid credential
# No Authorization header
401Response
{
  "error": "Unauthenticated.",
  "code": "UNAUTHENTICATED"
}

Errors

Redacted uses conventional HTTP status codes. Errors return a consistent JSON body with a human-readable error, a stable machine-readable code, and an optional detail. Internal stack traces and database messages are never exposed.

StatusCodeMeaning
200 / 201 / 202OK / Created / AcceptedThe request succeeded.
400BAD_REQUESTMalformed JSON or an invalid path parameter.
401UNAUTHENTICATEDMissing or invalid credential.
401CONFIRMATION_TOKEN_REQUIREDA high-risk action needs a confirmation token.
403FORBIDDENAuthenticated, but not allowed to act on this resource.
403INSUFFICIENT_SCOPEThe credential lacks a required scope.
422VALIDATION_ERRORThe body or query failed schema validation.
500INTERNAL_ERRORAn unexpected server error.
502WORKER_UNAVAILABLEThe scan / removal worker could not be reached.
Error shape
curl https://app.redacted.example/api/v1/scans \
  -X POST \
  -H "Authorization: Bearer $REDACTED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tiers": ["oops"] }'
403Response
{
  "error": "Policy check failed.",
  "code": "INSUFFICIENT_SCOPE",
  "detail": "missing scope: privacy.scan.write"
}

Scopes & permissions

Authorization is scope-based. Each credential carries an explicit set of scopes, and every handler asserts the required scope before it runs any query. Below are the scopes the API recognizes.

privacy.status.readRead exposure counts, removal status, and exposure lists/details.
privacy.scan.writeStart discovery scans and prepare removal campaigns.
privacy.removal.submitSubmit a prepared campaign (files removal requests). High-risk.
privacy.profile.writeUpsert non-sensitive identifiers (email, phone, alias…).
privacy.evidence.readGenerate signed URLs for before/after evidence. High-risk.
privacy.escalation.writeCreate escalation packets for non-compliant brokers.
privacy.drop.assistAssist drop / opt-out flows that require interactive steps.

High-risk write actions (submitting a campaign, writing sensitive profile fields, creating an escalation) additionally require an active mandate — the subject's signed authorization — and a short-lived confirmation token.

Scope enforcement
// Every handler runs assertCan(actor, action, ctx)
// before touching data. A credential without the
// required scope gets 403 INSUFFICIENT_SCOPE — the
// query never executes.

// Web sessions receive implicit scopes by role:
//   consumer → read + scan + submit + profile + evidence
//   admin    → the full consumer set

Pagination

List endpoints use limit / offset pagination. Responses include a pagination object with the effective limit, offset, and the total count of matching records, so you can page through the full set.

Query parameters

limitintegeroptional
Number of records to return. Defaults to 20, clamped to a maximum of 100.
offsetintegeroptional
Number of records to skip. Defaults to 0.
Paginated request
curl "https://app.redacted.example/api/v1/exposures?limit=20&offset=40" \
  -H "Authorization: Bearer $REDACTED_API_KEY"
200 OKResponse
{
  "exposures": [ /* … */ ],
  "pagination": {
    "limit": 20,
    "offset": 40,
    "total": 138
  }
}

Core resources

Status

A single summary of the subject's privacy-removal state: how many exposures exist, how many cases are running, how many removals are verified, any blockers, a status breakdown, and when the next scan is scheduled. Counts and IDs only — never PII.

GET/v1/status

Requires scope privacy.status.read

Query parameters

subjectIduuidoptional
Required for admin actors; ignored for consumer actors (their session subject is used).
blockersbooleanoptional
Include blocker details. Defaults to true.
GET /status
curl https://app.redacted.example/api/v1/status \
  -H "Authorization: Bearer $REDACTED_API_KEY"
200 OKResponse
{
  "exposureCount": 38,
  "runningCases": 4,
  "verifiedRemovedCount": 12,
  "blockers": [
    { "kind": "needs_user_action", "count": 1 }
  ],
  "statusBreakdown": {
    "found": 22,
    "request_sent": 4,
    "needs_user_action": 1,
    "verified_removed": 12
  },
  "nextScanAt": "2026-07-01T09:00:00.000Z"
}

Exposures

An exposure is a confirmed match of the subject's data on a broker surface. List them with current removal status and a suggested next action, or fetch one for its full removal history. Raw PII ciphertext is never returned — use the evidence endpoint for screenshots.

GET/v1/exposures

Requires scope privacy.status.read

Query parameters

statusenumoptional
Filter by removal status: found, request_sent, broker_confirmed, verified_removed, not_found, blocked, needs_user_action, legal_exception, escalated.
limitintegeroptional
Default 20, max 100.
offsetintegeroptional
Default 0.
subjectIduuidoptional
Admin only; consumers use their session subject.
GET/v1/exposures/:id

Requires scope privacy.status.read

Returns a single exposure with its broker and surface info, evidence hash, profile URL, and the full status progression of its removal.

GET /exposures
curl "https://app.redacted.example/api/v1/exposures?status=found&limit=20" \
  -H "Authorization: Bearer $REDACTED_API_KEY"
200 OKResponse
{
  "exposures": [
    {
      "id": "9b1f…c2",
      "broker": "Acme Data LLC",
      "domain": "acmedata.com",
      "profileUrl": "https://acmedata.com/p/…",
      "confidence": 0.92,
      "status": "found",
      "firstSeen": "2026-05-02T12:00:00.000Z",
      "lastSeen": "2026-06-15T12:00:00.000Z",
      "nextAction": "Review and confirm this is your profile…"
    }
  ],
  "pagination": { "limit": 20, "offset": 0, "total": 38 }
}
GET /exposures/:id
curl https://app.redacted.example/api/v1/exposures/9b1f…c2 \
  -H "Authorization: Bearer $REDACTED_API_KEY"

Scans

Start a discovery scan. The scan runs asynchronously in the removal worker, so the endpoint returns 202 Accepted immediately with a case and workflow id you can poll via status. Requires an active mandate with scan consent.

POST/v1/scans

Requires scope privacy.scan.write

Body parameters

tiersinteger[]optional
Broker tiers to scan. Defaults to [1] (highest-priority brokers).
subjectIduuidoptional
Admin only; consumers use their session subject.
POST /scans
curl https://app.redacted.example/api/v1/scans \
  -X POST \
  -H "Authorization: Bearer $REDACTED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tiers": [1, 2] }'
202 AcceptedResponse
{
  "caseId": "c4a7…e1",
  "workflowId": "wf_3f9b…",
  "status": "running",
  "estimatedNextUpdate": "2026-06-17T12:15:00.000Z"
}

Campaigns

A campaign bundles approved exposures into a set of removal requests. Preparing a campaign is a safe, reversible draft; submitting it is the high-risk step that actually files requests with brokers.

POST/v1/campaigns

Requires scope privacy.scan.write

Prepares a draft campaign from approved exposures. Files nothing yet. Requires mandate consent of delete or opt_out.

Body parameters

exposureIdsuuid[]optional
Exposures to include. If omitted, all approved hits are included.
subjectIduuidoptional
Admin only; consumers use their session subject.
POST/v1/campaigns/:id/submit

Requires scope privacy.removal.submit

Submits a prepared draft — transitions it to running and enqueues the removal workflow. This is a high-risk write: it requires an active mandate and a valid confirmation token. The campaign id acts as the idempotency key, so re-submitting is safe.

Body parameters

confirmationTokenstringrequired
A short-lived token (min 12 chars) from POST /v1/confirmations.
GET/v1/campaigns

Requires scope privacy.status.read

Lists campaigns for the subject, most recent first.

POST /campaigns
curl https://app.redacted.example/api/v1/campaigns \
  -X POST \
  -H "Authorization: Bearer $REDACTED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "exposureIds": ["9b1f…c2"] }'
201 CreatedResponse
{
  "draftCampaignId": "ca_71d2…",
  "requests": [
    { "broker": "Acme Data LLC", "method": "form" }
  ],
  "requiresConfirmation": true,
  "summary": { "brokerCount": 1, "fieldCount": 4 }
}
POST /campaigns/:id/submit
curl https://app.redacted.example/api/v1/campaigns/ca_71d2…/submit \
  -X POST \
  -H "Authorization: Bearer $REDACTED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "confirmationToken": "ct_9f8e7a6b…" }'
202 AcceptedResponse
{
  "caseId": "c4a7…e1",
  "workflowId": "wf_55ad…",
  "status": "running",
  "submittedCount": 1,
  "blockedCount": 0
}

Confirmations

High-risk actions require a short-lived confirmation token, scoped to one action on one object. The raw token is returned once and only its hash is stored — lose it and you simply request another. Tokens default to a 15-minute TTL.

POST/v1/confirmations

Requires scope (scope of target action)

Body parameters

actionenumrequired
One of removal.submit, escalation.write, profile.write.sensitive.
objectTypestringrequired
The kind of object being acted on, e.g. campaign.
objectIduuidrequired
The object being acted on.
ttlSecondsintegeroptional
Token lifetime. Defaults to 900, max 3600.
POST /confirmations
curl https://app.redacted.example/api/v1/confirmations \
  -X POST \
  -H "Authorization: Bearer $REDACTED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "removal.submit",
    "objectType": "campaign",
    "objectId": "ca_71d2…"
  }'
201 CreatedResponse
{
  "confirmationToken": "ct_9f8e7a6b5c4d…",
  "action": "removal.submit",
  "expiresAt": "2026-06-17T12:15:00.000Z"
}

Profiles

Read non-PII profile metadata, or upsert a non-sensitive identifier. The API never returns decrypted PII — only metadata such as jurisdiction, risk class, mandate state, and which kinds of identifier exist. Values are encrypted before they are written and never logged.

GET/v1/profiles

Requires scope privacy.status.read

Returns the subject summary: subjectId, jurisdiction, riskClass, activeMandate, and identifierKinds.

PATCH/v1/profiles

Requires scope privacy.profile.write

Upserts a non-sensitive identifier. Sensitive fields like address, date of birth, or identity-document details are not accepted here — they go through a separate sensitive-write flow that requires mandate consent and a confirmation token.

Body parameters

kindenumrequired
One of email, phone, alias, username, employer, relative.
valuestringrequired
1–512 characters. Encrypted at rest.
isCurrentbooleanoptional
Whether this is a current identifier. Defaults to true.
GET /profiles
curl https://app.redacted.example/api/v1/profiles \
  -H "Authorization: Bearer $REDACTED_API_KEY"
200 OKResponse
{
  "subjectId": "su_4f2a…",
  "jurisdiction": "US-CA",
  "riskClass": "standard",
  "activeMandate": true,
  "identifierKinds": ["email", "phone", "alias"]
}
PATCH /profiles
curl https://app.redacted.example/api/v1/profiles \
  -X PATCH \
  -H "Authorization: Bearer $REDACTED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "email",
    "value": "[email protected]"
  }'

Evidence

Returns a short-lived signed URL (15-minute TTL) for a before/after evidence artifact — the screenshots that prove a removal. Screenshots may contain PII, so this endpoint requires the dedicated privacy.evidence.read scope and every access is audit-logged.

GET/v1/evidence/:id

Requires scope privacy.evidence.read

Query parameters

typeenumoptional
Which artifact to sign: before (default) or after.
GET /evidence/:id
curl "https://app.redacted.example/api/v1/evidence/9b1f…c2?type=after" \
  -H "Authorization: Bearer $REDACTED_API_KEY"
200 OKResponse
{
  "url": "https://spaces.example/evidence/…?X-Amz-Signature=…",
  "type": "after",
  "expiresAt": "2026-06-17T12:15:00.000Z"
}

Ready to build with the Redacted API?

Request an API key, or run a free exposure scan to see the data the API works with before you write a line of code.