# CRD Agency API: developer quickstart

**Version 0.2.0 · Live CRD integration**

Every API customer uses a CRD agency account. Create an agency through `/create-agency`, complete onboarding and agency KYC, wait for the existing KYC review to approve the agency, then open /agency/api as the owner. The API account and assigned workspace are provisioned automatically; there is no separate API approval or manual enablement step. The owner manages keys, HTTPS webhook endpoints, usage, and configured billing tools at `/agency/api`. Account creation alone does not approve access.

Your backend sends creator information it is authorized to share. This API does not automatically retrieve another platform’s data. There is one assigned workspace per agency, and no public workspace creation operation.

```text
Your application → Your backend → CRD Agency API → CRD engine
Your application ← Your backend ← Results & signed webhooks
```

The live service uses real CRD creators, eligibility, scans, findings, and queued deindex records. Invitations send real email. Scans can trigger CRD’s existing reporting workflows. Missing configuration returns an error; the service never substitutes simulated results. Use authorized creator data when making live requests.

## 1. Get your connection details

The live API is served at `https://api.crd.systems`. During the preview, access is limited to agencies CRD has enabled. To try the examples against a copy of the service running on your own computer, set `CRD_BASE_URL` (for example `http://127.0.0.1:4318`).

Create a scoped live key in `/agency/api`. You can optionally give it a name of up to 80 characters to recognize the integration in your dashboard. Existing keys can remain unnamed; the name is a display label, never an authentication credential. Choose **30 days**, **90 days**, or **Does not expire**, and save the one-time secret on your server:

- `Authorization: Bearer crd_live_<64 hex characters>`
- `Content-Type: application/json` for JSON writes
- `Idempotency-Key` for scan and deindex creation

A `key_...` value is the dashboard record ID, not the secret. A `crd_test_...` key is rejected by the live service. Never embed keys in browser/mobile code or put them in commands, source control, analytics, or logs. Read them through your server’s environment or secret manager.

`GET /v1/me` returns `expires_at` as an epoch-millisecond timestamp, or `null` when the key has no scheduled expiration. A non-expiring key can still be revoked and remains subject to ownership, verification, and permission checks. Replace a key by issuing another, updating the backend secret, verifying a request, and revoking the previous key.

Every request requires current approved agency access. After an ownership change, the current verified owner opens /agency/api to establish the new binding and replace keys and webhook endpoints. Previous binding credentials cannot continue under the new owner. If server-side verification is unavailable, calls fail with `agency_verification_unavailable`; keeping the dashboard open is not required.

| Operation | Scope |
| --- | --- |
| Read assigned workspace | `workspaces:read` |
| Enroll or update / read creator roster | `creators:write` / `creators:read` |
| Enable / pause API scheduling | `protection:write` |
| Request / read scan | `scans:write` / `scans:read` |
| Read findings and deindex state | `cases:read` |
| Queue Google deindex request | `cases:read` and `deindex:write` |
| Read usage / event history | `usage:read` / `events:read` |

## Permission presets: Read only and Read and write

Create keys in `/agency/api`. The preset chooses what the integration may request; it does not complete KYC, approve a mandate, or activate protection. Call `GET /v1/me` to inspect the scopes actually issued to the key.

| Capability | Read only | Read and write |
| --- | --- | --- |
| Read workspace, creators, identities, and eligibility | Yes | Yes |
| Read scan progress, cases, and deindex state | Yes | Yes |
| Read usage estimates and event history | Yes | Yes |
| Invite/link creators and update their identity | No | Yes |
| Activate or pause API protection | No | Yes, subject to workflow requirements |
| Request a scan | No | Yes, for an active eligible creator |
| Request eligible API deindex processing | No | Yes, with supported provenance and eligibility |
| Create/revoke keys, manage webhooks or payments | Agency dashboard | Agency dashboard |
| Sign/approve mandates or perform administrator actions | No | No |

**Read only** includes `workspaces:read`, `creators:read`, `scans:read`, `cases:read`, `usage:read`, and `events:read`.

**Read and write** includes those six plus `creators:write`, `protection:write`, `scans:write`, and `deindex:write`.

A missing scope returns `403 forbidden`. No public endpoint upgrades a key. Issue a replacement with the needed preset, change the backend secret, verify it, then revoke the old key. Reading a roster does not activate its protection or start active-protection usage.

One agency key serves that agency's authorized creators. You do not need one key per creator. Software serving several independent CRD agencies must use a separate key and workspace for each agency. Your server sends the creator data it is authorized to share; the API does not automatically retrieve another platform's data.

## 2. Read your agency roster, then link or invite as needed

Call `GET /v1/creators?limit=25` to read the existing agency roster and its stable API creator IDs. These are the same creators used in the CRD agency dashboard. Missing API mappings are created automatically without activating scheduling or billing. Use the returned `next_cursor` CRD user UUID as `after` for another page. Reuse a returned creator ID rather than creating a second record.

`GET /v1/creators/{id}` refreshes names, aliases, and official profiles from CRD, so dashboard edits appear in API reads. To update that same identity from your backend, use `PATCH /v1/creators/{id}` with any non-empty subset of `display_name`, `aliases`, and `profile_urls`. Omitted fields stay unchanged. Aliases and platforms merge into the current collections; this operation does not remove the other existing entries.

### Link an existing creator with your external ID

Set `CRD_API_KEY` in your backend environment. Set `CRD_EXISTING_CREATOR_ID` to a real creator UUID already belonging to your agency, and `CRD_CREATOR_EXTERNAL_ID` to your stable record ID. This example links that creator and updates the supplied identity fields. Replace the fictional identity values before running it against a real profile. It does not activate scheduling or request a scan.

```js
const baseUrl = process.env.CRD_BASE_URL || 'https://api.crd.systems';
const key = process.env.CRD_API_KEY;
const crdUserId = process.env.CRD_EXISTING_CREATOR_ID;
const externalId = process.env.CRD_CREATOR_EXTERNAL_ID;
if (!key || !crdUserId || !externalId) {
  throw new Error('Set CRD_API_KEY, CRD_EXISTING_CREATOR_ID, and CRD_CREATOR_EXTERNAL_ID');
}

async function crd(path, { method = 'GET', body, headers = {} } = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
      ...headers,
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  const result = await response.json();
  if (!response.ok) {
    throw new Error(`${response.status} ${result.error?.code ?? 'request_failed'}`);
  }
  return result;
}

const connection = await crd('/v1/me');
if (connection.environment !== 'live' || !connection.engine_configured) {
  throw new Error('The live CRD engine connection is not ready');
}
const workspaceId = connection.workspaces[0]?.id;
if (!workspaceId) throw new Error('CRD must assign your agency workspace');

const creator = await crd('/v1/creators', {
  method: 'POST',
  body: {
    workspace_id: workspaceId,
    external_id: externalId,
    crd_user_id: crdUserId,
    display_name: 'Example Creator',
    aliases: ['example_creator'],
    profile_urls: ['https://onlyfans.com/example_creator'],
  },
});
console.log({ creatorId: creator.id, status: creator.protection_status });
```

Keep the complete enrollment payload stable across retries. A matching existing enrollment returns the same creator; changing fields under the same `external_id` returns `external_id_conflict`. If roster sync already imported that CRD user, enrollment reuses the stable API creator ID and can replace its automatic crd:UUID external ID with yours. A different external ID already assigned by an integration conflicts. No duplicate creator or usage interval is created. For later identity changes use PATCH. Arrays are order-sensitive. Enrollment alone does not activate the API schedule or start usage.

For a new creator, replace `crd_user_id` with `email`, `first_name`, `last_name`, and optional `locale` (`en` or `fr`). This enrollment request triggers the existing CRD invitation email; activating protection does not send that invitation. The creator must accept and complete the required portal steps.

`email_sent: true` means the email provider accepted the invitation email, not that it reached the inbox or that the creator accepted it. Invitations expire after seven days. An identical enrollment retry does not resend the invitation. There is no public API resend endpoint; use the existing agency invitation controls when a resend is needed. A false email_sent requires reviewing that invitation rather than repeatedly changing external IDs. An existing, confirmed account already in the agency can be linked without a new invitation. Read `GET /v1/creators/{id}` to resolve the invitation and current eligibility. `identity_sync_pending` indicates that legal onboarding must finish before submitted identity fields can be applied. An invitation is not an approved mandate or entitlement.

| Field | Rules |
| --- | --- |
| `workspace_id` | Your assigned agency workspace. |
| `external_id` | Stable ID, 1–200 characters, unique in the workspace. |
| `display_name` | 1–160 characters. |
| `aliases` | Up to 30 names/usernames, each 1–160 characters; merged CRD alias capacity also applies. |
| `profile_urls` | Up to 20 supported official HTTPS profile URLs, at most 2,048 characters each. |
| `crd_user_id` | Existing agency creator UUID, or use invitation fields instead. |
| `email` | Required for an invitation, at most 254 characters. |
| `first_name` / `last_name` | Required for an invitation, each 1–100 characters. |
| `locale` | Optional `en` or `fr`. |

Use `aliases` for pseudonyms, alternate spellings, and usernames without having to specify a social network. When you have an official URL, also send it in `profile_urls`. The existing integration extracts the platform and username into the native CRD creator identity: `https://onlyfans.com/example_creator` supplies `example_creator`, and `https://mym.fans/fr/example_mym` supplies `example_mym`.

The native leak scanner uses supported creator-platform handles, including OnlyFans and MYM, with alias variants. Social profiles remain part of the existing identity and impersonation workflows; adding a social URL does not make every social handle a leak-search query. For a network without supported API profile URLs, send the relevant username as an alias rather than submitting an unsupported URL.

Supported profile hosts are OnlyFans, MYM, Fansly, Stripchat, Chaturbate, ManyVids, RevealMe, Instagram, X/Twitter, and TikTok. Use a canonical creator profile URL without query, fragment, credentials, or custom port. Ordinary profiles use one username path; MYM accepts `mym.fans` and `mym.com` with an optional `en`/`fr` prefix (responses use `mym.fans`) and ManyVids uses `/Profile/{numeric_id}/{username}`. Handles are at most 100 characters. Stored aliases may be 160 characters, but the current scan variant builder uses aliases of 2–60 characters containing a Latin letter and selects up to two extra variants per handle. URLs are added as unverified identity data, not proof of ownership. Unknown payload fields are rejected. Media upload and biometric ingestion are not available.

## 3. Check eligibility and activate scheduling

Read `GET /v1/creators/{id}`. The response includes `authorization`, `entitlement`, and `eligible` from actual CRD state. The creator needs an approved, signed, unexpired mandate and an active entitlement. Complete those through the existing CRD workflow. There is no public operation to approve or sign a mandate.

Scanning also needs an API rate that CRD has set for your agency. Credentials, the roster, invitations and webhooks work without one; activating scheduling or requesting a manual scan before then returns `409 api_rate_not_configured`, and scheduled scanning stays paused until CRD enables it. A new agency also starts with a small active-creator capacity (25 by default); CRD raises it when your agreement is in place, and activation beyond it returns `409 capacity_reached`.

For an invited creator, the existing CRD invitation email leads to account acceptance and the creator dashboard. The dashboard asks for missing official accounts and presents the existing DMCA signing flow; CRD reviews the submitted authorization through its current approval process. The API does not create a second signing system. Agency creators use the existing in-app approval notification and observed creator.updated state; do not wait for a separate standard creator approval email. A creator with an existing valid approved mandate and entitlement can reuse that state.

The enrollment response includes `invitation_id` and `email_sent`, not a private invitation URL. While authorization is pending, show a waiting state and poll `GET /v1/creators/{id}`. After `creator.updated`, read the creator again for current enrollment, authorization, and entitlement state. Events reflect observed changes and do not replace the actual signing and review process.

**Activation is a separate request.** Accepting an invitation or approving the mandate does not automatically change `protection_status` to active. A creator can be linked and eligible while the API schedule is still not_activated. Read the latest state, then explicitly send `PUT /v1/creators/{id}/protection` with `status: active` using a Read and write key.

If your integration lets the agency select protection before onboarding finishes, store that choice in your own application. After a creator update, read current eligibility and send the active PUT only when eligible is true and the agency still wants protection. Do not infer eligibility from a successful invitation or from an email notification.

After eligibility, continue with the `crd` helper and `creator` above:

```js
const current = await crd(`/v1/creators/${creator.id}`);
if (!current.eligible) throw new Error('Complete CRD authorization and entitlement first');
await crd(`/v1/creators/${creator.id}/protection`, {
  method: 'PUT', body: { status: 'active' },
});
```

Activation enables the API-owned daily scan schedule and starts its usage interval. Eligibility is checked again when work executes. Pausing with `{ "status": "paused" }` stops future API scheduling, ends the interval, and cancels queued API work. It does not undo dispatched work or cancel the underlying CRD subscription and independent recurring protection.

### Several creators and partial completion

There is no bulk enrollment, activation, or scan endpoint. Use the same agency key for individual creator requests. For ten creators, retain ten distinct external IDs and returned API creator IDs. Process them with a bounded queue and preserve each creator's own state and errors.

| State | Next action |
| --- | --- |
| Invitation pending; no crd_user_id | Wait for acceptance/onboarding and refresh the creator. |
| Linked; identity_sync_pending true | Keep the record; submitted identity is waiting for the existing signed legal onboarding requirements. |
| authorization.approved false | The creator signs and CRD reviews through the existing portal workflow. |
| entitlement.active false | Resolve the native account or agency entitlement through CRD. |
| eligible true; not_activated or paused | Send the explicit active PUT if protection is desired. |
| active and eligible | Follow scheduled scans or request a manual scan. |
| Enrollment uncertain or failed | Read the error and reconcile the same external ID and original payload. |

One creator still awaiting authorization does not need to block another eligible creator. Do not recreate successful records when another item fails. Pending invitations may occupy native agency roster capacity, but they do not start the active API protection billing interval. Several aliases and URLs still belong to one creator profile.

Pausing stops future API scheduling and ends its usage interval. It does not delete the creator or recall work already dispatched. After eligibility is restored, read fresh state and explicitly activate again if desired.

## 4. Request a real scan and read findings

Persist a unique idempotency key with each logical scan and reuse it for network retries:

```js
const scan = await crd(`/v1/creators/${creator.id}/scans`, {
  method: 'POST',
  headers: { 'Idempotency-Key': 'creator-001-scan-001' },
  body: {},
});
const progress = await crd(`/v1/scans/${scan.id}`);
console.log({ scanId: scan.id, status: progress.status });
```

A new scan returns `202` while queued. Matching retries return `200` with the same scan. A different pending scan returns `scan_in_progress`. Follow `GET /v1/scans/{id}` or signed events until it completes. States include `queued`, `dispatching`, `running`, `uncertain`, `completed`, `failed`, and `cancelled`. `uncertain` means the worker must reconcile real provider effects; do not create a replacement with a new key.

After completion, read `GET /v1/creators/{id}/cases?limit=25`. Results have `simulated: false` and reflect synchronized native CRD leak records for the creator. Known API job provenance is preserved; native findings without it have `scan_id: null` and `source: "crd"`. Zero findings is a valid result. Use `after=LAST_CASE_ID` for another page, with a maximum limit of 100. A finding is not a removal confirmation.

Once the native invitation is accepted, the canonical creator profile belongs to the agency and appears under that agency in the existing admin Leaks client list, even before a scan finds anything. Native findings remain associated with the same creator, and the team continues its normal review and processing workflow.

The cases collection synchronizes the creator's native CRD leak records, including existing findings and findings from manual, scheduled, and API scans. The API retains stable case IDs and reads current native status. Findings without API-scan provenance have `scan_id: null` and `source: "crd"`; those with known API scan mapping retain it. `case.created`, `case.updated`, and `case.deleted` describe records first observed, changed, or deleted during synchronization. API deindex requests have their own endpoint for current processing state.

Check `api_deindex_supported` before offering an API deindex action. A native finding can later gain a verified API scan mapping while keeping the same API case ID. `updated_at` uses epoch milliseconds; native `actionable` and `dead_on_arrival` flags can be null before they are observed.

## 5. Queue a Google deindex request

For an eligible returned case with verified API-scan provenance, send `POST /v1/cases/{id}/deindex-requests` with `{ "provider": "google" }` and a stable `Idempotency-Key`. A new request returns `202`. Retrieve it through `GET /v1/deindex-requests/{id}`.

This creates a real pending CRD record after checking current eligibility and exact API-scan finding provenance. Native findings without that mapping continue through the existing admin workflow; this API route returns `409 native_case_managed_by_crd` for them. **The operation does not itself send the Google notice.** Further processing is required. Keep `status`, `submitted_to_provider`, and `removal_verified` distinct in your interface. Queued is not submitted; submitted is not verified removal. Reuse the same idempotency key and input when retrying uncertain responses.

## 6. Receive signed HTTPS webhooks

In `/agency/api`, register a publicly reachable HTTPS receiver on port 443 and save its one-time signing secret. Private, loopback, reserved destinations, and redirects are rejected. Registration applies to future events.

Events: `protection.activated`, `protection.paused`, `scan.completed`, `scan.failed`, `creator.updated`, `case.created`, `case.updated`, `case.deleted`, and `deindex.status_changed`.

These cover current agency creators linked through the API, their synchronized native leak records, API scheduling/scans, and API deindex requests. `creator.updated` includes public identity, enrollment, authorization, and entitlement state, not legal documents or signatures. Synchronization runs on reads and through periodic worker reconciliation. Initial observation can emit `creator.updated`; importing an existing finding emits `case.created`. These are observed changes, not a transaction log: edits between checks may be combined into one update. Refresh the resource after receiving an event. A creator transferred out of the agency is no longer readable or deliverable to the former agency. Envelopes contain `id`, `type`, `workspace_id`, `created_at` in epoch milliseconds, `simulated: false`, and type-specific `data`.

`creator.updated` carries `data.creator_id` and the public `data.creator` object. `case.updated` and native `case.created` carry `data.case_id`, `data.creator_id`, `data.status`, and the public `data.case` object. Some API-scan `case.created` events contain only IDs and status. `case.deleted` carries the case and creator IDs; remove or archive the record in your local view. Deleting a CRD record is not proof that content was removed from a website.

Verify `X-CRD-Signature: t=UNIX_SECONDS,v1=HEX_HMAC_SHA256` using HMAC-SHA256 over `timestamp + '.' + original_request_bytes` with the endpoint secret. Use constant-time comparison and a timestamp tolerance. Verify before JSON parsing, deduplicate `event.id`, persist before responding `2xx`, and retrieve current resource state when applying an event. `X-CRD-Event-ID` contains the same ID.

Failed delivery retries use exponential backoff, up to five attempts. Replay terminal deliveries through the authenticated dashboard. Recover events with `GET /v1/events?limit=25&after=LAST_EVENT_ID`. Save the last returned event ID even when `next_cursor` is null. See the marketing documentation’s Webhooks tab for a complete verifier example.

### Troubleshoot a missing webhook

- Register the endpoint before starting work. New endpoints receive future events, not automatic backfill of old deliveries.
- Check Recent deliveries in `/agency/api`. Delivered means the receiver returned 2xx, not that your downstream job finished.
- Accept public HTTPS on port 443 without redirects. The current delivery timeout is five seconds; verify and durably enqueue before responding promptly.
- Retry failures use exponential backoff, up to five attempts. Replay only a terminal failed/delivered record through the dashboard; the event ID stays the same.
- Recover recorded events with `GET /v1/events` and refresh current resources. Periodic synchronization may combine edits into one observed state.
- `deindex.status_changed` is emitted when the workflow status changes. Refresh the deindex resource to reconcile outcome flags even when no separate event arrived for a flag change.
- Current agency verification, ownership, and creator membership still gate delivery. A creator transferred to another agency is no longer delivered to the former agency.

There are no separate public billing, invoice-paid, KYC-approved, invitation-accepted, or mandate-approved event types. Observed creator eligibility changes appear in `creator.updated`. Webhook signing secrets are separate from API keys; replacing one does not replace the other.

## 7. Read usage and configured billing

`GET /v1/usage?month=YYYY-MM` measures linked creators with active API scheduling in the UTC month. One full active month is one creator-month; partial months are prorated by active time. Aliases and ordinary requests are not separate creator profiles. `billing_basis` is `active_api_scan_schedule`. Enrollment alone is not metered.

Prices are configured by CRD under the agency agreement. Unconfigured prices are null and do not create an extra API permission gate. The response is an estimate, not an invoice or payment confirmation. Usage does not change CRD subscriptions.

Use `/agency/api` for the billing tools enabled by CRD. A live Stripe connection requires explicit server configuration. Where enabled, payment-method setup and the customer portal use that live account; CRD administrators may create a real draft invoice for a completed month. Draft creation does not finalize, send, or charge the invoice automatically. Confirm the collection process with CRD before onboarding paying clients.

## Pagination and event recovery

All three list endpoints default to 25 items and accept a limit from 1 to 100. Send the returned `next_cursor` as `after` until it is null. Do not infer the cursor from a display name, array index, or unrelated resource ID.

| Collection | Cursor |
| --- | --- |
| `GET /v1/creators` | Returned native CRD user UUID, not the crt_ API ID. |
| `GET /v1/creators/{id}/cases` | Returned API case ID from that creator's collection. |
| `GET /v1/events` | Returned event cursor while paging; save the last processed event ID for later polling. |

Current access filtering can leave an empty page with a non-null cursor, so follow the cursor even if data is empty. When no newer event is returned, retain the previously saved event position. These are changing live collections, not frozen exports; deduplicate by stable IDs.

## Errors and retries

Errors normally return `{ "error": { "code": "...", "message": "..." }, "request_id": "req_..." }`. Some ingress failures include only the code. Live responses identify their mode through `X-CRD-Mode: live`. Record status/code/request ID without logging secrets.

- Agency setup, KYC, ownership, and binding errors require the corresponding CRD account action.
- `crd_engine_not_configured` or provider unavailability requires CRD to restore the live connection.
- `creator_invitation_pending` and `creator_not_eligible` require actual creator onboarding or eligibility resolution.
- `creator_rebinding_required` requires review after an agency binding change.
- `external_id_conflict` means enrollment input changed under an existing ID.
- `scan_in_progress` means follow existing work; `scan_limit_reached` means wait for the rolling allowance.
- `api_rate_not_configured` means CRD has not yet enabled scanning for your agency; contact CRD. `capacity_reached` means the agency's active-creator capacity is full.
- `partner_scan_limit_reached` means the agency used its daily manual-scan budget across all creators; scheduled scans are unaffected.
- `key_limit_reached` and `webhook_limit_reached` mean revoke or disable one you no longer use.
- `idempotency_conflict` means reuse the original input for that key.

Persist the complete request before sending enrollment. Preserve field order, omitted defaults, array order, and values when replaying the original payload. A changed payload under the same external ID conflicts; use PATCH for later identity changes.

Scan idempotency keys are unique across the agency's scan operations, and deindex keys are unique across the agency's deindex operations. They are not per-creator counters. Use 1–120 characters, a new key for each new logical operation, and the original key for its retries. A matching retry may return 200 instead of the original 201/202; it does not start another operation.

| Failure | Next step |
| --- | --- |
| 400 invalid input | Correct the documented field or URL format; do not retry unchanged. |
| 401 unauthorized | Check the complete live secret, expiration, revocation, and current account access. |
| 403 forbidden | Check GET /v1/me scopes and use the appropriate key; account verification errors require their reported account action. |
| 409 workflow/conflict | Inspect the code and current creator/request state; resolve the specific requirement. |
| 429 rate_limited | Pause the agency queue and retry with backoff and jitter. The allowance is shared across its keys. |
| 429 scan_limit_reached | Wait for that creator's rolling 24-hour manual-scan allowance. |
| 429 partner_scan_limit_reached | Wait for the agency's rolling 24-hour manual-scan budget; scheduled scans continue. |
| Network failure or 502/503/504 | Back off on reads; preserve original operation identity when retrying writes. |

Retry reads with backoff. Honor `Retry-After` when present; the live API does not guarantee that header. Preserve external IDs and idempotency keys across write retries. The defaults are 120 requests per agency account per fixed minute shared by its keys, 16 KiB request bodies, one pending scan per creator, five manual scans per creator (and 50 per agency) per rolling 24 hours, at most 20 active keys and 10 active webhook endpoints per agency, and five webhook delivery attempts. These are implementation defaults, not a service-level commitment.

The live contract intentionally excludes sandbox fixture routes and authorization submission/approval routes. Disposable development fixtures remain separate from customer data, credentials, and the live database.
