# Datasource Wholesale Background Check API > Order background checks from your own platform with the applicant's full details (name, date of birth, SSN, current address), billed to your account at wholesale prices. You collect the applicant's details, the disclosure and their written authorization; we run the check and send signed webhooks as it progresses. Nothing is sent to the applicant by us. REST + JSON, API key auth. ## Rules for AI agents - Base URLs: sandbox https://sandbox.datasourcecorp.app (keys pk_test_...), production https://api.datasourcecorp.app (keys pk_live_...). Send the key as `X-Api-Key: pk_test_YOUR_KEY` (or `Authorization: Bearer ...`), from a server only. Never put a key or an SSN in browser or mobile code. - Order with POST /clients/account/checks/direct. `account` is the caller's own account; a sub-account (optional, POST /clients) is addressed by its `clientId` (`cl_…`) or the caller's `externalClientId`. Every check is billed to the caller's account. - Every order needs `level` (1 to 6, from GET /products), `applicant` (firstName, lastName, dateOfBirth `YYYY-MM-DD`, ssn, address { street, city, state, postalCode }; middleName when there is one) and `certification` { permissiblePurpose: "employment" | "volunteer", disclosureProvided: true, authorizationObtained: true }. Only set the certification to true when the integrating application really collected the disclosure acknowledgment and the written authorization; build that step into the applicant flow. - Never log request bodies or SSNs. Error messages name failing fields and never echo values. - Some levels need `jobLocation` and `stateOfEmployment` (both or neither). Driving-record levels need `applicant.driversLicense` { number, state }. - Always send your own `externalIdentifier` (≤ 100 characters, unique across the account and its sub-accounts) and reuse it on every retry: a retry never creates a second check. A `409 duplicate_check` on a retry means the first attempt worked: use `order` from the body. Duplicates are also detected by SSN on the same account. - Never set `forceReorder` after a `502`. A 502 whose body is a check (`status` PENDING) resolves on its own within about an hour; a 502 with `code` `upstream_unavailable` created nothing and is safe to retry. - Statuses: PENDING, CREATED (stage `in_progress`), then CLEAR, REVIEW, ERROR (with `errorCode`, `errorMessage`) or CANCELLED. CLEAR/REVIEW carry `expirationDate`, `reportUrl`; finished checks carry `charges` (USD). - Errors are RFC 9457 problem details with a stable `code`; branch on HTTP status, then `code`. Never on `title` or `detail`. - Verify `X-Partner-Signature` on every webhook: lowercase hex HMAC-SHA256 of the raw body, keyed with the whole `whsec_...` secret, compared in constant time. Answer 2xx within 15 seconds. - Webhooks are at least once and unordered: drop repeats by `eventId`; for each check keep the state with the latest `timestamp`. - Register the webhook URL with PUT /settings (https only), and test it with POST /settings/webhook-test. - Run a nightly reconciliation for `account` and each sub-account: GET /clients/{clientRef}/checks?updated_since=...&limit=500, following `nextCursor` until null. - Show prices with their `feesNote` ("Plus pass-through fees."). - Ignore unknown response fields, enum values and webhook events. ## Docs - [Integration guide](https://api.datasourcecorp.app/docs/guide.md): concepts, ordering, certification, tracking, webhooks, compliance - [Error catalog](https://api.datasourcecorp.app/docs/errors.md): every `code`, its HTTP status, and whether a retry is safe - [Changelog](https://api.datasourcecorp.app/docs/changelog.md): dated API changes - [OpenAPI 3.1](https://api.datasourcecorp.app/openapi/v1.json): machine-readable reference with examples - [Interactive reference](https://api.datasourcecorp.app/scalar) - [Everything in one file](https://api.datasourcecorp.app/llms-full.txt): this file, the guide, the error catalog, a section per operation and per webhook with full JSON examples The sandbox serves the same documents at https://sandbox.datasourcecorp.app, plus its sandbox-only routes. --- # Datasource Wholesale Background Check API: Integration Guide Order background checks from your own platform with the applicant's full details, billed to your account at wholesale prices. You run the applicant-facing flow (collecting their details, the disclosure and their authorization); we run the check and tell you, with signed webhooks, as it moves. This guide covers everything end to end. The interactive reference is at [/scalar](https://api.datasourcecorp.app/scalar), the OpenAPI 3.1 document at [/openapi/v1.json](https://api.datasourcecorp.app/openapi/v1.json), and the [error catalog](https://api.datasourcecorp.app/docs/errors.md) lists every error `code`. ## 1. Start here | Environment | Base URL | API keys | Checks | |---|---|---|---| | Test (sandbox) | `https://sandbox.datasourcecorp.app` | `pk_test_...` | Not real; nothing is charged | | Production | `https://api.datasourcecorp.app` | `pk_live_...` | Real and billed | 1. `GET /status` with your test key. `200` means the key works. 2. `PUT /settings` with your webhook URL, then `POST /settings/webhook-test` to check that your endpoint verifies our signature. 3. `GET /products`: the levels (1 to 6) you can order, at your prices. 4. `POST /clients/account/checks/direct` with the applicant's details, a `level` and your `certification`. 5. Handle `check.updated` webhooks, and reconcile nightly with `GET /clients/account/checks?updated_since=...`. ```bash curl https://sandbox.datasourcecorp.app/status -H "X-Api-Key: pk_test_YOUR_KEY" ``` ## 2. Concepts - **Account.** Your own account. In any path, `{clientRef}` `account` means it. Every check is billed to it, at your wholesale prices. - **Sub-account** (optional). `POST /clients` creates one under your account, to keep one of your customers' checks, reports and portal users apart from the rest. Address it by our `clientId` (`cl_…`) or your own `externalClientId`. Checks ordered on a sub-account are still billed to your account. You never need one to order. - **Check.** One background check for one applicant. It has a contract-stable `status` and a finer `stage`. Send your own `externalIdentifier` (unique across your account and sub-accounts) and key your records by our `id` (`chk_…`). - **Level.** What to order: levels 1 to 6 (`GET /products`). We translate the level to the product on the account. - **Certification.** On every order you certify the permissible purpose, that you gave the applicant the disclosure, and that you have their written authorization (section 4). ## 3. Authentication Send your key on every request as `X-Api-Key: ` or `Authorization: Bearer `. Keys are issued by Datasource and shown once: keep them in a secret manager and use them only from your servers, never in a browser or mobile app. You can have two active keys at once, so you can rotate without downtime (ask support for the second key, deploy it, then ask us to revoke the first). A missing, wrong or revoked key is `401 invalid_api_key`. A `pk_test_` key doesn't work on production, and the reverse. Repeated invalid keys from one IP address get `429 rate_limited` for a minute; a valid key is never throttled that way. ## 4. Ordering a check ```bash curl https://sandbox.datasourcecorp.app/clients/account/checks/direct \ -H "X-Api-Key: pk_test_YOUR_KEY" -H "Content-Type: application/json" \ -d '{ "externalIdentifier": "app-20931", "level": 2, "applicant": { "firstName": "Jordan", "middleName": "Lee", "lastName": "Kim", "dateOfBirth": "1990-04-12", "ssn": "078-05-1120", "email": "jordan.kim@example.com", "address": { "street": "1200 Larimer St", "street2": "Apt 4", "city": "Denver", "state": "CO", "postalCode": "80202" } }, "certification": { "permissiblePurpose": "employment", "disclosureProvided": true, "authorizationObtained": true }, "jobLocation": "Denver, CO", "stateOfEmployment": "CO" }' ``` `201 Created` returns the check (`status` `CREATED`) and a `Location` header. The check is billable from here. ### The applicant | Field | Required | Notes | |---|---|---| | `firstName`, `lastName` | yes | Legal name, at most 100 characters each. | | `middleName` | when there is one | Omit if the applicant has none. Some searches match on it. | | `dateOfBirth` | yes | `YYYY-MM-DD`. The applicant must be 14 to 120 years old. | | `ssn` | yes | 9 digits, with or without dashes. Numbers the SSA never issues (area `000`, `666` or `9xx`, group `00`, serial `0000`) are rejected. | | `address` | yes | The current home address: `street`, optional `street2`, `city`, `state` (2-letter), `postalCode` (`80202` or `80202-1234`). | | `email`, `phone` | no | Kept with the order. Nothing is sent to them. | | `driversLicense` | for driving-record levels | `{ "number": "...", "state": "CO" }`. | ### The certification Every order carries one. As the user of the report you are responsible for these steps, and you certify them per order: - `permissiblePurpose`: `employment` (hiring, promotion, retention, or engaging a contractor) or `volunteer`. - `disclosureProvided`: `true` means you gave the applicant a clear written disclosure, in a document consisting only of that disclosure, that a background check may be obtained. - `authorizationObtained`: `true` means you have the applicant's written authorization, and keep it. An order without a complete certification is `400 validation_failed` and nothing is billed. We keep the purpose and the time you certified with the check. ### Job location Some levels need `jobLocation` (for example `Denver, CO`) and `stateOfEmployment` (`CO`): where the applicant will work, which decides the state rules that apply. Send both or neither, and only what you actually know. ### Duplicates and retries - An order is `409 duplicate_check` (nothing ordered or billed) when the same `externalIdentifier`, or the same applicant (matched by SSN) on the same account, already has a check in progress (`PENDING`, `CREATED`) or completed and not expired (`CLEAR`, `REVIEW`). The body carries the existing check in `order`. - `forceReorder: true` orders anyway and is billed as a new check. - Always send an `externalIdentifier` and reuse it when you retry after a timeout or network error. A retry never creates a second check: you get the check, or `409 duplicate_check` with it (your first attempt worked). - A `502` whose body is a check (`status` `PENDING`) means the order was sent but its answer was lost. Don't use `forceReorder`. It resolves on its own within about an hour and you get `check.updated`. ### Handling applicant data - Send orders from your servers only, over TLS. The API accepts nothing else. - Don't log request bodies, and keep the SSN out of your own error reports. - We pass the details to the screening provider for that one order and never store or log them. The SSN is kept only as a keyed one-way hash, so duplicate orders can be caught. Our error messages name the fields that failed and never repeat the values you sent. ## 5. Products and prices `GET /products` lists every level with its name, description and your price. `GET /clients/{clientRef}/products` lists what one account or sub-account can order. Show prices with their `feesNote` ("Plus pass-through fees."): some jurisdictions charge court or state fees that are passed through at cost and appear in `charges` once a check finishes. ## 6. Tracking a check | `status` | Meaning | Final? | |---|---|---| | `PENDING` | Being submitted, or its submission's answer was lost (resolves on its own). | no | | `CREATED` | Submitted; searches are running. `stage` is `in_progress`. | no | | `CLEAR` | Complete, and no search found records. | yes | | `REVIEW` | Complete, with records to review. | yes | | `ERROR` | Couldn't be completed. `errorCode` and `errorMessage` say why. | yes | | `CANCELLED` | Cancelled by you. | yes | - `CLEAR` and `REVIEW` checks carry `completedAt`, `expirationDate` (completion plus `checkValidityYears`, default 2; set it with `PUT /settings` or per sub-account with `PUT /clients/{clientRef}/settings`) and `reportUrl`, the report in the Datasource portal. - Finished checks carry `charges` (US dollars). - Read one check with `GET /clients/{clientRef}/checks/{checkRef}` (`checkRef` is our `id` or your `externalIdentifier`). Cancel one that is still open with `POST .../{checkRef}/cancel`. - Reconcile with `GET /clients/{clientRef}/checks?updated_since=...&limit=500`, following `nextCursor` until it is null. Do it nightly for `account` and every sub-account: it catches anything a webhook missed. ## 7. Webhooks Register `https://` URL with `PUT /settings`. Every event is a `POST` of a flat JSON object with `event`, `eventId` and `timestamp`: - `check.updated`: a check's status or stage changed. Carries the check. - `check.expiring`: 30 days and again 7 days before a `CLEAR` or `REVIEW` check's `expirationDate`. - `ping`: sent by `POST /settings/webhook-test`. **Verify every delivery.** Compute HMAC-SHA256 over the raw body (the exact bytes, before parsing), keyed with the UTF-8 bytes of your whole webhook secret (`whsec_` prefix included), hex-encode it in lowercase, and compare it with `X-Partner-Signature` in constant time. ```js // Node.js 20+ (rawBody is a Buffer of the exact request body) import crypto from "node:crypto"; const expected = crypto.createHmac("sha256", process.env.DATASOURCE_WEBHOOK_SECRET).update(rawBody).digest("hex"); const given = Buffer.from(req.get("X-Partner-Signature") ?? "", "utf8"); const valid = given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, "utf8")); ``` ```python # Python 3 import hashlib, hmac, os expected = hmac.new(os.environ["DATASOURCE_WEBHOOK_SECRET"].encode("utf-8"), raw_body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, request.headers.get("X-Partner-Signature", "")) ``` Answer with any `2xx` within 15 seconds and do slow work afterwards. Anything else is retried up to 5 more times with increasing delays. Delivery is at least once and unordered: a retry resends the identical body (same `eventId` and `timestamp`), so drop repeats by `eventId`, and keep the state with the latest `timestamp` for each check. `GET /settings/webhook-deliveries` shows the last 30 days of attempts and how your endpoint answered. Rotate the secret with `POST /settings/webhook-secret` (the old one stops working at once). ## 8. Errors Every error is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details with a stable `code` and a `type` link to its entry in the [error catalog](https://api.datasourcecorp.app/docs/errors.md). Branch on the HTTP status, then on `code`; never on `title` or `detail`. ## 10. Going live 1. Run your test integration end to end: an order, every status through the simulator, signature checks, a nightly reconciliation. 2. Ask Datasource for your `pk_live_` key and set your production webhook URL with `PUT /settings` on `https://api.datasourcecorp.app` (settings are per environment). 3. Make sure your disclosure and authorization flow is the one you certify on every order. ## 11. Limits | What | Limit | |---|---| | `externalIdentifier`, `externalClientId` | 100 characters | | Names | 100 characters each | | Page size (`limit`) | 1 to 500 | | Webhook answer | 15 seconds | | Invalid-key requests from one IP | 20 a minute, then `429` for a minute | ## 12. Versioning and changes Changes are additive: new fields, new optional parameters, new webhook events, new enum values. Ignore what you don't know. A breaking change gets 90 days' notice in the [changelog](https://api.datasourcecorp.app/docs/changelog.md) and by email. ## 13. Compliance Datasource is the consumer reporting agency. You are the user of the report: you certify a permissible purpose on every order, give the applicant a standalone written disclosure and get their written authorization before you order, and you make, and are responsible for, every decision based on a report. That includes the adverse action process: a pre-adverse action notice with a copy of the report and the summary of rights before you decide, and an adverse action notice after. State and local rules (for example ban-the-box and fair chance laws) can add steps; follow the rules where the applicant will work. --- # Error catalog Every error response is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details (`Content-Type: application/problem+json`) with a stable `code`. Its `type` is `https://api.datasourcecorp.app/docs/errors.md#`, a link to the entry below. Branch on `code`: titles and details are for people and may change. ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'acct-7731' exists under your account.", "code": "client_not_found" } ``` Validation errors (`validation_failed`, `invalid_cursor`) also carry `errors`: an object from field name to a list of messages, for example `{ "applicant.ssn": ["applicant.ssn must be a valid 9-digit Social Security number."] }`. Messages never repeat the values you sent. Two documented responses are not problem details: - `409` on `POST /clients/{clientRef}/checks/direct` with `code` `duplicate_check`: the body is `{ code, message, currentStatus, instructions, order }`, with the existing check in `order`. - `502` on `POST /clients/{clientRef}/checks/direct` when the body is a check (`status` `PENDING` or `ERROR`, no `code`). See [upstream_unavailable](#upstream_unavailable). Read the HTTP status first, then `code` when the body has one. ## All codes | `code` | HTTP | Retry the same request? | What to do | |---|---|---|---| | `validation_failed` | 400 | No | Fix each field named in `errors`, then send again. | | `invalid_cursor` | 400 | No | Start again from the first page (omit `cursor`). | | `invalid_json` | 400 | No | Send a valid JSON body with `Content-Type: application/json`, and values of the right type. | | `invalid_api_key` | 401 | No | Use a valid key for this environment (`pk_test_` on the sandbox, `pk_live_` on production). | | `not_available_for_account_type` | 403 | No | The operation isn't part of the wholesale API. Use the operations in this reference. | | `client_not_found` | 404 | No | Use `account`, or a sub-account's `clientId` or `externalClientId`. | | `check_not_found` | 404 | No | Check the `id` or `externalIdentifier`, and the account it was ordered on. | | `not_found` | 404 | No | The route doesn't exist. Check the method and path. | | `method_not_allowed` | 405 | No | Use the method the reference lists for this path. | | `duplicate_check` | 409 | No | Use the existing check in `order`. Only send `forceReorder: true` if you want a new, billed check. | | `external_client_id_in_use` | 409 | No | Use the existing sub-account: `GET /clients?externalClientId=...`. | | `external_identifier_in_use` | 409 | No | Use a different `externalIdentifier` (it is unique across your account and sub-accounts). | | `payload_too_large` | 413 | No | Send a smaller body. | | `unsupported_media_type` | 415 | No | Send `Content-Type: application/json`. | | `level_not_available` | 422 | No | Pick a level from `GET /clients/{clientRef}/products`, or contact support. | | `rate_limited` | 429 | Yes, after `Retry-After` | Wait the number of seconds in `Retry-After`, then send again. | | `internal_error` | 500 | Yes, with the same `externalIdentifier` | Retry with backoff; contact support if it persists. | | `upstream_unavailable` | 502 | Usually; see the entry | Retry with backoff. For orders, read the entry first. | | `request_failed` | other | Depends | Any other framework error. Read `status` and `detail`. | ## validation_failed **400.** The request is invalid: a required field is missing or malformed (an SSN that can't be issued, a date of birth that isn't `YYYY-MM-DD`, an unknown state), a value is too long, or the `certification` is incomplete. `errors` lists each problem by field path (`applicant.address.postalCode`, `certification.authorizationObtained`). Nothing was created or billed. **Retry:** only after fixing the request. ## invalid_cursor **400.** On `GET /clients/{clientRef}/checks`, `cursor` is not a `nextCursor` we issued. **Retry:** no; start again from the first page. ## invalid_json **400.** The body isn't valid JSON, is missing, or a value has the wrong type (for example `"level": "two"`), or a query value can't be read. Nothing was created. **Retry:** after fixing the request. ## invalid_api_key **401.** The API key is missing, invalid or revoked, or it belongs to the other environment. **Retry:** only with a valid key. Repeated invalid keys from one IP address lead to `rate_limited`. ## not_available_for_account_type **403.** The operation exists on this server but isn't part of the wholesale API (for example ordering by invitation, or the hosted connect page). Nothing was done. **Retry:** no. ## client_not_found **404.** `{clientRef}` isn't `account` or one of your sub-accounts. Other customers' accounts always look like this. **Retry:** no. ## check_not_found **404.** No check with that `id` or `externalIdentifier` exists on that account. **Retry:** no. ## not_found **404.** No such route. Check the method, the path, and that you're calling the right environment (some routes exist only in the sandbox). **Retry:** no. ## method_not_allowed **405.** The path exists but not with this HTTP method. **Retry:** no. ## duplicate_check **409.** On `POST /clients/{clientRef}/checks/direct`: the applicant (same `externalIdentifier`, or same SSN on the same account) already has a check that is in progress (`PENDING`, `CREATED`) or completed and not yet expired (`CLEAR`, `REVIEW`). Nothing was ordered or billed. The body is not problem details: it is `{ code, message, currentStatus, instructions, order }` with the existing check in `order`. If you were retrying after a timeout, your first attempt worked: use `order`. **Retry:** no. `forceReorder: true` orders anyway and is billed as a new check. ## external_client_id_in_use **409.** On `POST /clients`: another of your sub-accounts already has this `externalClientId`. Nothing was created. **Retry:** no; use the existing sub-account. ## external_identifier_in_use **409.** The `externalIdentifier` is taken. On an order: it belongs to a check on another of your accounts; nothing was ordered. On `PATCH /clients/{clientRef}/checks/{checkRef}`: the check already has one (it can be set once), or another check uses the value; nothing changed. **Retry:** no; use a different value. ## payload_too_large **413.** The request body is larger than the server accepts. **Retry:** no. ## unsupported_media_type **415.** The body isn't sent as `Content-Type: application/json`. **Retry:** no; set the header. ## level_not_available **422.** The account has no product for that level. Nothing was ordered. **Retry:** no; pick a level from `GET /clients/{clientRef}/products`, or contact support. ## rate_limited **429.** Too many requests with an invalid API key came from your IP address (20 a minute; valid keys are never throttled this way). `Retry-After` gives the seconds to wait. **Retry:** yes, after waiting. ## internal_error **500.** Something failed on our side. The body has a `traceId` to quote to support and never contains error details. For an order, the check may or may not exist: retry only with the same `externalIdentifier`, and expect either a normal result or `409 duplicate_check`. **Retry:** yes, with backoff. ## upstream_unavailable **502.** The screening provider didn't accept the request. As problem details, nothing was created or recorded and a retry is safe. **For an order the body can instead be the check itself** (`status` `PENDING`, no `code`): the order was sent but its answer was lost, so it may or may not exist upstream. Don't retry with `forceReorder`. Retry only with the same `externalIdentifier`, or just wait: a `PENDING` check is looked up automatically and resolved within about an hour, and you get `check.updated` either way. **If the body is a check with `status` `ERROR`**, the screening provider refused that order outright (`errorCode` and `errorMessage` say why). Nothing was created or billed; order again once the cause is fixed. ## request_failed **Any other status** the framework produces (rare). Read `status` and `detail`. **Retry:** depends on the status. --- # Endpoint reference One section per operation, in the reference's tag order, generated from `/openapi/v1.json`. Every operation needs your API key (`X-Api-Key: pk_test_YOUR_KEY` or `Authorization: Bearer pk_test_YOUR_KEY`). Error bodies are problem details; see the error catalog above. ## Account ### GET /status **Check your API key** (operationId `CheckApiKey`) Returns `200` with a short message when the API key is valid. Useful for verifying setup and key rotation. Responses: - **`200`**: The key is valid. The body is plain text. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ## Settings ### GET /settings **Get your settings** (operationId `GetSettings`) Returns the webhook URL and the connect return URLs you registered, and how many years a completed check stays valid. Responses: - **`200`**: Your registered webhook URL and connect return URLs. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "webhookUrl": "https://app.example.com/cbc/webhooks", "connectReturnUrls": [ "https://app.example.com/cbc/connect-done" ] } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### PUT /settings **Update your settings** (operationId `UpdateSettings`) Registers where we send webhooks. `webhookUrl` must be an https:// URL whose host resolves only to public addresses; it is checked again whenever a webhook is sent. Send an empty `webhookUrl` to stop webhooks (poll instead). This call replaces your settings; send the complete set. (`connectReturnUrls` isn't used by wholesale accounts.) `checkValidityYears` (1 to 5, default 2) is how long a `CLEAR` or `REVIEW` check stays valid: its `expirationDate` is its completion plus that many years, and until then ordering the same applicant again needs `forceReorder`. Omit it to keep the current value. A change applies to checks completed after it. Request body (`application/json`): `SettingsRequest` | Field | Type | Required | Description | |---|---|---|---| | `webhookUrl` | `string (uri) | null` | no | https:// URL that receives webhooks. Its host must resolve only to public addresses. Empty or omitted turns webhooks off. | | `connectReturnUrls` | `string (uri)[] (≤ 10 items) | null` | no | Not used by wholesale accounts. | | `checkValidityYears` | `integer (int32; 1 to 5) | null` | no | Your account's default for how many years a `CLEAR` or `REVIEW` check stays valid: its `expirationDate` is its completion plus this. A whole number from 1 to 5. A client's own setting (`PUT /clients/{clientRef}/settings`) overrides it. Omitted keeps the current value (default 2). Applies to checks completed after the change. | Request example: ```json { "webhookUrl": "https://app.example.com/cbc/webhooks", "connectReturnUrls": [ "https://app.example.com/cbc/connect-done" ] } ``` Responses: - **`200`**: The saved settings. Both values were replaced with what you sent. - **`400`**: `validation_failed`: a URL isn't `https://`, has credentials or a fragment, or its host resolves to a private address, or there are more than 10 return URLs. Nothing was changed. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "webhookUrl": "https://app.example.com/cbc/webhooks", "connectReturnUrls": [ "https://app.example.com/cbc/connect-done" ] } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "webhookUrl": [ "webhookUrl must use https://." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### GET /settings/webhook-deliveries **List recent webhook deliveries** (operationId `ListWebhookDeliveries`) Every attempt to deliver a webhook to your endpoint in the last 30 days, newest first: the event, its `eventId`, the check (for check events), your endpoint's HTTP status and how long it took. Use it to see what was sent and how your endpoint answered. Filter with `eventId` or `checkId`; `limit` is 1 to 200 (default 50). Bodies are never stored, ours or yours: re-send a check's current state with `POST /clients/{clientRef}/checks/{checkRef}/replay-webhook`. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | query | `integer (int32)` | no | Page size, 1 to 500 (larger values are clamped). Defaults to 100. | | `eventId` | query | `string` | no | | | `checkId` | query | `string` | no | | Responses: - **`200`**: OK - **`400`**: Bad Request - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `400` example (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "level": [ "level is required." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### POST /settings/webhook-secret **Rotate your webhook secret** (operationId `RotateWebhookSecret`) Creates a new webhook signing secret and returns it. **It is shown only once**; store it before switching your signature check over. The old secret stops working immediately, so webhooks sent before you switch will fail verification until you do. Responses: - **`200`**: The new secret, shown only this once. The old one stopped working immediately. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "webhookSecret": "whsec_YOUR_SECRET" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### POST /settings/webhook-test **Send a test webhook** (operationId `TestWebhook`) Sends a signed `ping` event to your webhook URL, so you can verify your endpoint and your signature check. Delivery is asynchronous and retried like any other webhook. Responses: - **`202`**: A signed `ping` was queued for delivery to your webhook URL. - **`400`**: `validation_failed`: no webhook URL is registered. Register one with `PUT /settings` first. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `202` example (`application/json`): ```json { "status": "QUEUED" } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "webhookUrl": [ "Register a webhookUrl with PUT /settings first." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ## Clients ### GET /clients **List clients** (operationId `ListClients`) Returns your account (`account`, the first item) and every sub-account, oldest first. Pass `externalClientId` to look up one sub-account by your own id: the list then holds that sub-account, or nothing. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `externalClientId` | query | `string` | no | Return only the client with this `externalClientId` (your own id for the customer, at most 100 characters). The list then has 0 or 1 items. | Responses: - **`200`**: Your account first, then your sub-accounts, oldest first. With `externalClientId`, a list of 0 or 1 items. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json [ { "clientId": "cl_2e4c6a8b0d1f3e5a7c9b1d3f", "externalClientId": "acct-7731", "name": "Northwind Staffing - Denver", "status": "Active", "createdAt": "2026-11-14T15:04:05Z", "portalUrl": "https://portal.example.com" } ] ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### POST /clients **Create a client** (operationId `CreateClient`) Creates a sub-account under your account. You don't need one to order: order on `account`. Use sub-accounts to keep one of your customers' checks, reports and portal users apart from the rest. Checks ordered on a sub-account are billed to your account, at your prices. A portal login is created for the primary contact, and Datasource emails them their username and a temporary password (changed at first sign-in), so they can read their reports. No password is ever returned to you; `welcomeEmailSent` says whether the email went. Send your own id for the customer as `externalClientId` and you never have to store ours: every `{clientRef}` path accepts it. It is unique among your clients (`409` if already used). Request body (`application/json`): `CreateClientRequest` | Field | Type | Required | Description | |---|---|---|---| | `externalClientId` | `string (≤ 100 chars) | null` | no | Your own identifier for this customer (max 100 characters, unique among your clients). Lets you address the client by your id instead of ours: use it wherever `{clientRef}` appears. | | `legalCompanyName` | `string` | yes | Legal registered entity name of the company or organization. | | `dbaCompanyName` | `string | null` | no | Optional 'Doing Business As' (DBA) or trade name. | | `primaryContactFirstName` | `string` | yes | First name of the primary administrator contact. | | `primaryContactLastName` | `string` | yes | Last name of the primary administrator contact. | | `primaryContactEmail` | `string (email)` | yes | Email address of the primary contact. Becomes the username of their portal login. | | `primaryContactPhone` | `string | null` | yes | Phone number of the primary contact. | | `companyPhone` | `string | null` | no | Optional main company phone number. | | `streetLine1` | `string` | yes | Physical street address (line 1). | | `streetLine2` | `string | null` | no | Optional physical street address (line 2, suite/unit). | | `city` | `string` | yes | Physical city. | | `state` | `string` | yes | State or province: 2-letter code (e.g. `CO`) or full name. | | `postalCode` | `string` | yes | Physical postal / ZIP code. | | `country` | `string` | no | Country code (US or CA). Defaults to `US`. | | `billingStreetLine1` | `string | null` | no | Optional separate billing street address (line 1). If omitted, physical address is used. | | `billingStreetLine2` | `string | null` | no | Optional separate billing street address (line 2). | | `billingCity` | `string | null` | no | Optional separate billing city. | | `billingState` | `string | null` | no | Optional separate billing state. | | `billingPostalCode` | `string | null` | no | Optional separate billing postal / ZIP code. | | `billingCountry` | `string | null` | no | Optional separate billing country code (defaults to 'US'). | Request example: ```json { "externalClientId": "acct-7731", "legalCompanyName": "Northwind Staffing LLC", "dbaCompanyName": "Northwind Staffing - Denver", "primaryContactFirstName": "Riley", "primaryContactLastName": "Chen", "primaryContactEmail": "riley.chen@northwind.example", "primaryContactPhone": "(303) 555-0142", "streetLine1": "1600 Champa St", "streetLine2": "Suite 500", "city": "Denver", "state": "CO", "postalCode": "80202", "country": "US" } ``` Responses: - **`201`**: The client was created, with a portal login for its primary contact. The contact gets their username and a temporary password by email from Datasource; no password is ever returned to you. `welcomeEmailSent` is false if that email couldn't be sent (contact support). - **`400`**: `validation_failed`: a required field is missing or too long. `errors` names each field. Nothing was created; fix the request and send it again. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`409`**: `external_client_id_in_use`: another of your clients already has this `externalClientId`. Nothing was created; use the existing client (`GET /clients?externalClientId=`). - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: `upstream_unavailable`: the screening provider didn't answer. Nothing was recorded on our side; it's safe to retry. `201` example (`application/json`): ```json { "clientId": "cl_2e4c6a8b0d1f3e5a7c9b1d3f", "externalClientId": "acct-7731", "name": "Northwind Staffing - Denver", "status": "ACTIVE", "createdAt": "2026-11-14T15:04:05Z", "portalUrl": "https://portal.example.com", "primaryUsername": "riley.chen@northwind.example", "welcomeEmailSent": true } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "primaryContactEmail": [ "primaryContactEmail is required." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `409` example `external_client_id_in_use` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#external_client_id_in_use", "title": "External client id in use", "status": 409, "detail": "Another of your clients already uses externalClientId 'org-1042'.", "code": "external_client_id_in_use" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` `502` example `upstream_unavailable` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#upstream_unavailable", "title": "Screening provider unavailable", "status": 502, "detail": "The screening provider did not accept the request. Nothing was created; it is safe to retry.", "code": "upstream_unavailable" } ``` ### GET /clients/{clientRef} **Get a client** (operationId `GetClient`) Returns one of your clients. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | Responses: - **`200`**: The client. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found`: no client with that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "clientId": "cl_2e4c6a8b0d1f3e5a7c9b1d3f", "externalClientId": "acct-7731", "name": "Northwind Staffing - Denver", "status": "Active", "createdAt": "2026-11-14T15:04:05Z", "portalUrl": "https://portal.example.com" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `client_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'org-1042' exists under your account.", "code": "client_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### GET /clients/{clientRef}/settings **Get a client's settings** (operationId `GetClientSettings`) Returns the client's own settings and the values that apply to it (its own, else your account's defaults). Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | Responses: - **`200`**: OK - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: Not Found - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'org-1042' exists under your account.", "code": "client_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### PUT /clients/{clientRef}/settings **Update a client's settings** (operationId `UpdateClientSettings`) Sets one client's own settings. This call replaces them; send the complete set. `checkValidityYears` (1 to 5) is how long this client's `CLEAR` or `REVIEW` checks stay valid: their `expirationDate` is completion plus that many years, and until then ordering the same applicant again needs `forceReorder`. Null or omitted uses your account's default (`checkValidityYears` in `PUT /settings`). A change applies to checks completed after it. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | Request body (`application/json`): `ClientSettingsRequest` | Field | Type | Required | Description | |---|---|---|---| | `checkValidityYears` | `integer (int32; 1 to 5) | null` | no | How many years this client's `CLEAR` or `REVIEW` checks stay valid: their `expirationDate` is completion plus this. A whole number from 1 to 5, or null (or omitted) to use your account's default (`checkValidityYears` in `PUT /settings`). Applies to checks completed after the change. | Responses: - **`200`**: OK - **`400`**: Bad Request - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: Not Found - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `400` example (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "level": [ "level is required." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'org-1042' exists under your account.", "code": "client_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ## Products ### GET /clients/{clientRef}/products **List a client's product levels** (operationId `ListClientProducts`) Returns the product levels this client can order, priced for this client (fees can differ per client). Levels the client doesn't have are left out. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | Responses: - **`200`**: The levels this client can order, priced for it. Levels it doesn't have are left out. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found`: no client with that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: `upstream_unavailable`: the screening provider didn't answer. Safe to retry. `200` example (`application/json`): ```json [ { "level": 2, "name": "Standard", "description": "National criminal database, county search, SSN trace.", "price": 34.95, "feesNote": "Plus pass-through fees." } ] ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `client_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'org-1042' exists under your account.", "code": "client_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` `502` example `upstream_unavailable` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#upstream_unavailable", "title": "Screening provider unavailable", "status": 502, "detail": "The screening provider did not accept the request. Nothing was created; it is safe to retry.", "code": "upstream_unavailable" } ``` ### GET /products **List product levels** (operationId `ListProducts`) Returns every product level (1 to 6) you can order, sorted by level, with its name, description and base price. Send the `level` when ordering a check; you never need to know which product that maps to. Prices are base prices: third-party fees are added ("Plus pass-through fees."). Fees can differ per client, so use `GET /clients/{clientRef}/products` to show a client's own prices. Responses: - **`200`**: Every level, 1 to 6, with its base price from your account's products. Show `feesNote` next to prices. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: `upstream_unavailable`: the screening provider didn't answer. Safe to retry. `200` example (`application/json`): ```json [ { "level": 2, "name": "Standard", "description": "National criminal database, county search, SSN trace.", "price": 34.95, "feesNote": "Plus pass-through fees." } ] ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` `502` example `upstream_unavailable` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#upstream_unavailable", "title": "Screening provider unavailable", "status": 502, "detail": "The screening provider did not accept the request. Nothing was created; it is safe to retry.", "code": "upstream_unavailable" } ``` ## Checks ### GET /clients/{clientRef}/checks **List checks** (operationId `ListChecks`) Returns a client's checks, oldest change first, one page at a time. Use it for reconciliation, e.g. a nightly job that catches any webhook you missed. Pass `updated_since` to get only checks that changed at or after that time, and follow `nextCursor` until it is null. A check that changes while you are paging may appear again on a later page, so process items idempotently (by `id`). Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | | `updated_since` | query | `string (date-time)` | no | Only return checks whose `updatedAt` is at or after this time. ISO 8601; a time with no offset is treated as UTC. Omit for all checks. | | `limit` | query | `integer (int32)` | no | Page size, 1 to 500 (larger values are clamped). Defaults to 100. | | `cursor` | query | `string` | no | The `nextCursor` from the previous page. Omit for the first page. An unknown value is `400 invalid_cursor`. | Responses: - **`200`**: One page of the client's checks, oldest change first. Follow `nextCursor` until it is null. - **`400`**: `invalid_cursor`: `cursor` isn't a `nextCursor` we issued; start again from the first page. (`invalid_json`: `updated_since` or `limit` isn't a valid value.) - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found`: no client with that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "items": [ { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "stage": "complete", "level": 2, "expirationDate": "2028-10-18T18:22:41Z", "completedAt": "2026-10-18T18:22:41Z", "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-18T18:22:41Z" } ], "nextCursor": "NjM4OTYxNjU3NjE0MDAwMDAwOjEwNDI" } ``` `400` example `invalid_cursor` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_cursor", "title": "One or more validation errors occurred.", "status": 400, "errors": { "cursor": [ "cursor is not valid. Use the nextCursor value from a previous page." ] }, "code": "invalid_cursor" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `client_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'org-1042' exists under your account.", "code": "client_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### POST /clients/{clientRef}/checks/direct **Order a background check** (operationId `OrderDirectCheck`) Orders a background check with the applicant's full details. Nothing is sent to the applicant: you collect their details, give them the disclosure and get their written authorization before you order, and certify that you did in `certification`. The details go to the screening provider for this order only; we never store or log them (the SSN is kept only as a keyed one-way hash, to catch duplicate orders). **Where it's billed.** Order on your own account with `{clientRef}` `account`: every check is billed to you at your wholesale prices. If you created sub-accounts (`POST /clients`) to keep your customers' checks apart, order on one with its `clientId` or your `externalClientId`; those checks are billed to you as well. **What to order.** Send the `level` (1 to 6, see `GET /products`). `422 level_not_available` means the account doesn't have that level (contact support). Levels that include a driving record need `applicant.driversLicense`. **Job location.** Some levels need `jobLocation` and `stateOfEmployment` (where the applicant will work; send both or neither). They decide which state's rules apply. **Duplicate protection.** An order is rejected with `409 Conflict` (and nothing is billed) when the same `externalIdentifier`, or the same applicant (by SSN) on this account, already has a check that is in progress (`PENDING`, `CREATED`) or completed and not yet expired (`CLEAR`, `REVIEW`). Set `forceReorder` to order anyway; that is billed as a new check. **Retries.** Always send an `externalIdentifier`, and reuse it when retrying after a timeout or network error: a retry can never create a second check. **502 responses.** If submission to the screening provider fails, the check is kept as `PENDING` and returned in the body; it resolves on its own (`check.updated` follows). Don't retry with `forceReorder`. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | Request body (`application/json`): `DirectOrderRequest` | Field | Type | Required | Description | |---|---|---|---| | `externalIdentifier` | `string (≤ 100 chars) | null` | no | Your unique identifier for this check (max 100 characters). Strongly recommended: retries with the same value never create a second check, and it is echoed in responses and webhooks. | | `level` | `integer (int32; 1 to 6) | null` | yes | Product level to order, 1 to 6 (see `GET /products`). | | `applicant` | `object | null | DirectApplicant` | yes | The applicant's identifying details. Used for this order only; never stored or logged by us. | | `certification` | `object | null | OrderCertification` | yes | Your certification for this order. Every order needs it; an order without a complete certification is rejected and nothing is billed. | | `jobLocation` | `string (≤ 100 chars) | null` | no | Where the applicant will work, e.g. `Denver, CO` (at most 100 characters). Some levels require it with `stateOfEmployment`; send both or neither. | | `stateOfEmployment` | `string | null` | no | The US state the applicant will work in (the employment jurisdiction), as a 2-letter code such as `CO`. Send with `jobLocation`. | | `orderedBy` | `object | null | OrderedBy` | no | Optional: who ordered this check. It is written to the order's notes so your staff can see who asked. Never stored by us and never creates a login. | | `forceReorder` | `boolean` | no | Order even if this applicant (matched by SSN) already has an in-progress or unexpired check. Defaults to false. A forced re-order is billed as a new check. | Request example: ```json { "externalIdentifier": "app-20931", "level": 2, "applicant": { "firstName": "Jordan", "middleName": "Lee", "lastName": "Kim", "dateOfBirth": "1990-04-12", "ssn": "078-05-1120", "email": "jordan.kim@example.com", "phone": "303-555-0187", "address": { "street": "1200 Larimer St", "street2": "Apt 4", "city": "Denver", "state": "CO", "postalCode": "80202" } }, "certification": { "permissiblePurpose": "employment", "disclosureProvided": true, "authorizationObtained": true }, "jobLocation": "Denver, CO", "stateOfEmployment": "CO", "orderedBy": { "email": "hr@northwind.example", "firstName": "Riley", "lastName": "Chen" }, "forceReorder": false } ``` Responses: - **`201`**: The check was submitted (`status` `CREATED`). Billable. `Location` points at the check. Nothing was sent to the applicant. - **`400`**: `validation_failed`: a field is missing or invalid, or the certification is incomplete. `errors` names each field; it never repeats the values you sent. Nothing was ordered. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`403`**: `not_available_for_account_type`: this key belongs to an invitation-based (CHMS) account, which orders with `POST /clients/{clientRef}/checks`. - **`404`**: `client_not_found`: `{clientRef}` isn't `account` or one of your sub-accounts. Don't retry. - **`409`**: Nothing was ordered or billed. Two cases, told apart by `code`: `duplicate_check` (this body: this applicant, matched by SSN, already has a check in progress or unexpired on this account, in `order`), or `external_identifier_in_use` (problem details: the `externalIdentifier` belongs to a check on another of your accounts; use a different one). - **`422`**: `level_not_available`: the account has no product for that level. Contact support. Nothing was ordered. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: Two cases. If the body is a check (`status` `PENDING`, no `code`), the submission's answer was lost: the check may exist upstream, is kept `PENDING`, and resolves on its own within about an hour (`check.updated` follows). Retry only with the same `externalIdentifier`, never with `forceReorder`. If the body is problem details with `upstream_unavailable`, nothing was created, and a retry is safe. `201` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CREATED", "stage": "awaiting_applicant", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T15:10:01Z" } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "applicant.ssn": [ "applicant.ssn must be a valid 9-digit Social Security number." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `403` example `not_available_for_account_type` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#not_available_for_account_type", "title": "Not available for your account type", "status": 403, "detail": "This operation is for wholesale accounts.", "code": "not_available_for_account_type" } ``` `404` example `client_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#client_not_found", "title": "Client not found", "status": 404, "detail": "No client 'acct-7731' exists under your account.", "code": "client_not_found" } ``` `409` example `duplicate_check` (`application/json`): ```json { "code": "duplicate_check", "message": "A background check is currently in progress with status 'CREATED'.", "currentStatus": "CREATED", "instructions": "To force a re-order, submit the request with 'forceReorder': true.", "order": { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "app-20931", "clientId": "cl_7d1a3c5e9b2f4a6c8e0d1b3f", "status": "CREATED", "stage": "in_progress", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-11-14T15:10:00Z", "updatedAt": "2026-11-14T15:10:01Z" } } ``` `422` example `level_not_available` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#level_not_available", "title": "Level not available", "status": 422, "detail": "This client has no product for level 4. Contact support.", "code": "level_not_available" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` `502` example `pending_check` (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "app-20931", "clientId": "cl_7d1a3c5e9b2f4a6c8e0d1b3f", "status": "PENDING", "stage": "submitting", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-11-14T15:10:00Z", "updatedAt": "2026-11-14T15:10:00Z" } ``` ### GET /clients/{clientRef}/checks/{checkRef} **Get a check** (operationId `GetCheck`) Returns one check. `checkRef` may be the check's `id` (`chk_…`) or your `externalIdentifier`. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | | `checkRef` | path | `string` | yes | The check: its `id` (`chk_…`) or your `externalIdentifier`. Resolves only among your own checks (of that client, where the path names one). | Responses: - **`200`**: The check's current state. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CREATED", "stage": "awaiting_applicant", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T15:10:01Z" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `check_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#check_not_found", "title": "Check not found", "status": 404, "detail": "No check 'vol-8841' exists for client 'org-1042'.", "code": "check_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### PATCH /clients/{clientRef}/checks/{checkRef} **Set your identifier on a check** (operationId `UpdateCheck`) Sets `externalIdentifier` on a check that doesn't have one yet, so you can link a check (for example one found with `POST /clients/{clientRef}/checks/lookup`) to your own record. `409` if the check already has an identifier, or if another of your checks uses that value. `checkRef` is the check's `id` (`chk_…`). Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | | `checkRef` | path | `string` | yes | The check: its `id` (`chk_…`) or your `externalIdentifier`. Resolves only among your own checks (of that client, where the path names one). | Request body (`application/json`): `UpdateCheckRequest` | Field | Type | Required | Description | |---|---|---|---| | `externalIdentifier` | `string (≤ 100 chars) | null` | yes | Your identifier for the check (max 100 characters). Allowed only while the check has none, and it must be unused by your other checks. | Request example: ```json { "externalIdentifier": "vol-8841" } ``` Responses: - **`200`**: The check, now with your `externalIdentifier`. - **`400`**: `validation_failed`: `externalIdentifier` is missing or longer than 100 characters. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`409`**: `external_identifier_in_use`: the check already has an `externalIdentifier` (it can be set once), or another of your checks uses that value. Nothing changed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CREATED", "stage": "awaiting_applicant", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T15:10:01Z" } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#validation_failed", "title": "One or more validation errors occurred.", "status": 400, "errors": { "externalIdentifier": [ "externalIdentifier is required." ] }, "code": "validation_failed" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `check_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#check_not_found", "title": "Check not found", "status": 404, "detail": "No check 'vol-8841' exists for client 'org-1042'.", "code": "check_not_found" } ``` `409` example `external_identifier_in_use` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#external_identifier_in_use", "title": "External identifier in use", "status": 409, "detail": "Another of your checks already uses that externalIdentifier.", "code": "external_identifier_in_use" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### POST /clients/{clientRef}/checks/{checkRef}/cancel **Cancel a check** (operationId `CancelCheck`) Cancels a check, e.g. one ordered by mistake. Cancel before the applicant submits their information to avoid charges. `checkRef` may be the check's `id` (`chk_…`) or your `externalIdentifier`. The check is marked `CANCELLED` either way. `upstreamCancelled` tells you whether the screening provider confirmed it; if it's false, the check may still be processed, so contact support. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | | `checkRef` | path | `string` | yes | The check: its `id` (`chk_…`) or your `externalIdentifier`. Resolves only among your own checks (of that client, where the path names one). | Responses: - **`200`**: The check is now `CANCELLED`. If `upstreamCancelled` is false the screening provider didn't confirm it and the check may still be processed and billed: contact support. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CANCELLED", "stage": "closed", "level": 2, "expirationDate": null, "completedAt": "2026-10-16T16:00:00Z", "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T16:00:00Z", "upstreamCancelled": true } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `check_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#check_not_found", "title": "Check not found", "status": 404, "detail": "No check 'vol-8841' exists for client 'org-1042'.", "code": "check_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` ### POST /clients/{clientRef}/checks/{checkRef}/replay-webhook **Re-send a check's webhook** (operationId `ReplayWebhook`) Queues a new delivery of the `check.updated` webhook with the check's current state, e.g. after your endpoint was down. Delivery is asynchronous and retried like any other webhook. `checkRef` may be the check's `id` (`chk_…`) or your `externalIdentifier`. Parameters: | Name | In | Type | Required | Description | |---|---|---|---|---| | `clientRef` | path | `string` | yes | Where the check is ordered: `account` (your own account), or a sub-account's `clientId` (`cl_…`) or your `externalClientId` for it. Every check is billed to your account. Anything else is `404 client_not_found`. | | `checkRef` | path | `string` | yes | The check: its `id` (`chk_…`) or your `externalIdentifier`. Resolves only among your own checks (of that client, where the path names one). | Responses: - **`200`**: A new `check.updated` delivery with the check's current state was queued (a new `eventId`). - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "message": "Webhook replay request accepted", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "status": "QUEUED" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#invalid_api_key", "title": "Invalid API key", "status": 401, "detail": "The API key is missing, invalid or revoked.", "code": "invalid_api_key" } ``` `404` example `check_not_found` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#check_not_found", "title": "Check not found", "status": 404, "detail": "No check 'vol-8841' exists for client 'org-1042'.", "code": "check_not_found" } ``` `429` example `rate_limited` (`application/problem+json`): ```json { "type": "https://api.datasourcecorp.app/docs/errors.md#rate_limited", "title": "Too many requests", "status": 429, "detail": "Too many requests with an invalid API key from your IP address. Wait for the number of seconds in the Retry-After header.", "code": "rate_limited" } ``` # Webhooks Every webhook is a `POST` of a flat JSON object with `event`, `eventId` and `timestamp`, signed in `X-Partner-Signature` (lowercase hex HMAC-SHA256 of the raw body, keyed with your whole `whsec_...` secret). Respond `2xx` within 15 seconds. Delivery is at least once: drop repeats by `eventId`. ## Webhook `check.updated`: Check status changed Sent to your webhook URL whenever a check changes status (including each step of a batch and automatic resolution of a `PENDING` check), and when you call **Re-send a check's webhook**. The payload carries both `status` and the finer `stage`. **Verify every delivery** before trusting it: compute HMAC-SHA256 over the raw request body (the exact bytes you received, before parsing), keyed with the UTF-8 bytes of your whole webhook secret (`whsec_` prefix included), hex-encode it in lowercase, and compare it to the `X-Partner-Signature` header in constant time. ```js // Node.js 20+ (rawBody is a Buffer of the exact request body) import crypto from "node:crypto"; const expected = crypto.createHmac("sha256", process.env.DATASOURCE_WEBHOOK_SECRET).update(rawBody).digest("hex"); const given = Buffer.from(req.get("X-Partner-Signature") ?? "", "utf8"); const valid = given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, "utf8")); ``` ```python # Python 3 import hashlib, hmac, os expected = hmac.new(os.environ["DATASOURCE_WEBHOOK_SECRET"].encode("utf-8"), raw_body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, request.headers.get("X-Partner-Signature", "")) ``` ```csharp // C# var expected = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), rawBody)).ToLowerInvariant(); var valid = CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signatureHeader ?? "")); ``` **Delivery.** Respond with any `2xx` within 15 seconds; do slow work asynchronously. Anything else is retried up to 5 more times with increasing delays. Delivery is at least once and not ordered: drop repeats by `eventId` (the same on every retry of one event), and keep the state with the latest `timestamp` for each check (key checks by `id`). | Field | Type | Required | Description | |---|---|---|---| | `event` | `string (one of "check.updated")` | no | Event type. Currently always `check.updated`. | | `eventId` | `string (uuid)` | no | Unique id of this event, the same on every retry of its delivery. Use it to drop duplicate deliveries. | | `id` | `string` | no | Identifier of the check (`chk_…`, same as `id` in API responses). | | `externalIdentifier` | `string | null` | no | Your identifier for the check, as sent when ordering. The easiest way to match the webhook to your records. | | `clientId` | `string` | no | Client the check belongs to (its `clientId`). | | `status` | `string (one of "QUEUED", "PENDING", "CREATED", "CLEAR", "REVIEW", "ERROR", "CANCELLED")` | no | `QUEUED`: in a batch, not yet submitted. `PENDING`: accepted, being submitted. `CREATED`: submitted; the applicant has been invited to complete their information. `CLEAR`: completed, no search found records. `REVIEW`: completed and flagged (records found, or the result needs a person to look). `ERROR`: could not be completed. `CANCELLED`: cancelled. `CLEAR`, `REVIEW`, `ERROR` and `CANCELLED` are final. | | `stage` | `string (one of "queued", "submitting", "awaiting_applicant", "applicant_ready", "in_progress", "complete", "closed")` | no | Where the check is, finer than `status`: `queued` (in a batch, not yet sent), `submitting` (being sent), `applicant_ready` (held: someone must review and release the order in the Datasource portal, e.g. a possible duplicate or a jurisdiction question), `in_progress` (searches are running), `complete` (final result ready: `CLEAR` or `REVIEW`), `closed` (will not complete: `ERROR` or `CANCELLED`). `applicant_ready` and `in_progress` are both `CREATED`; each move between them sends `check.updated`. | | `level` | `integer (int32; 1 to 6) | null` | no | Product level the check was ordered with (1 to 6), or null for a check imported from the account's history. | | `expirationDate` | `string (date-time) | null` | no | When a completed check expires (UTC). | | `completedAt` | `string (date-time) | null` | no | When the check reached a final status (UTC). | | `reportUrl` | `string | null` | no | Only for `CLEAR` and `REVIEW`: the report in the Datasource portal (needs a portal sign-in). Null otherwise. | | `charges` | `number (double) | null` | no | Only for a final status: what the screening provider charged, in US dollars. Null otherwise. | | `errorCode` | `string | null` | no | Only for `ERROR`: why, as a stable code (see the check's `errorCode`). Null otherwise. | | `errorMessage` | `string | null` | no | Only for `ERROR`: one sentence for your staff saying what went wrong and what to do. Null otherwise. | | `timestamp` | `string (date-time)` | no | When this webhook was generated (UTC). Deliveries can arrive out of order; use the latest `timestamp` per check. | Example: ```json { "event": "check.updated", "eventId": "e7a1c3b5-2d4f-4a6b-8c9d-0e1f2a3b4c5d", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "stage": "complete", "level": 2, "expirationDate": "2028-10-18T18:22:41Z", "completedAt": "2026-10-18T18:22:41Z", "timestamp": "2026-10-18T18:22:42Z" } ``` ## Webhook `check.expiring`: Check expiring soon Sent 30 days and again 7 days before a `CLEAR` or `REVIEW` check's `expirationDate` (each once; a check found later than 30 days out gets only the 7-day one). Use it to re-screen before the clearance lapses. Same signature, retry and `eventId` rules as `check.updated`. | Field | Type | Required | Description | |---|---|---|---| | `event` | `string` | no | Always `check.expiring`. | | `eventId` | `string` | no | Unique id of this event, the same on every retry of its delivery. Use it to drop duplicate deliveries. | | `timestamp` | `string (date-time)` | no | When this webhook was generated (UTC). | | `id` | `string` | no | The check's `id` (`chk_…`). | | `externalIdentifier` | `string | null` | no | Your identifier for the check, as sent when ordering. | | `clientId` | `string` | no | The account or sub-account the check belongs to (`cl_…`). | | `status` | `string` | no | `CLEAR` or `REVIEW`. | | `expirationDate` | `string (date-time)` | no | When the check expires (UTC). | | `daysLeft` | `integer (int32)` | no | Whole days until `expirationDate` (30 or fewer). | Example: ```json { "event": "check.expiring", "eventId": "a4c6e8f0-2b4d-4f6a-8c0e-1a3c5e7f9b2d", "timestamp": "2028-09-18T14:00:03Z", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "expirationDate": "2028-10-18T18:22:41Z", "daysLeft": 30 } ``` ## Webhook `ping`: Test ping Sent by `POST /settings/webhook-test` so you can verify your endpoint and your signature check. Same signature, retry and `eventId` rules as `check.updated`. | Field | Type | Required | Description | |---|---|---|---| | `event` | `string (one of "ping")` | no | Always `ping`. | | `eventId` | `string (uuid)` | no | Unique id of this event, the same on every retry of its delivery. Use it to drop duplicate deliveries. | | `timestamp` | `string (date-time)` | no | When this webhook was generated (UTC). | Example: ```json { "event": "ping", "eventId": "f1d3b5a7-9c2e-4d6f-8a0b-3c5e7f9a1b2d", "timestamp": "2026-10-16T15:00:00Z" } ``` --- # Changelog Changes to the wholesale API, newest first. Changes are additive; a breaking change gets 90 days' notice here and by email. ## 2026-11-14 - First release: direct ordering with the applicant's full details (`POST /clients/{clientRef}/checks/direct`), a per-order certification (permissible purpose, disclosure, authorization), duplicate detection by SSN, ordering on your own account (`account`) or optional sub-accounts, `check.updated`, `check.expiring` and `ping` webhooks, nightly reconciliation, and the sandbox simulator.