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,
the OpenAPI 3.1 document at /openapi/v1.json, and the
error catalog 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 |
GET /statuswith your test key.200means the key works.PUT /settingswith your webhook URL, thenPOST /settings/webhook-testto check that your endpoint verifies our signature.GET /products: the levels (1 to 6) you can order, at your prices.POST /clients/account/checks/directwith the applicant's details, aleveland yourcertification.- Handle
check.updatedwebhooks, and reconcile nightly withGET /clients/account/checks?updated_since=....
curl https://sandbox.datasourcecorp.app/status -H "X-Api-Key: pk_test_YOUR_KEY"
2. Concepts
- Account. Your own account. In any path,
{clientRef}accountmeans it. Every check is billed to it, at your wholesale prices. - Sub-account (optional).
POST /clientscreates one under your account, to keep one of your customers' checks, reports and portal users apart from the rest. Address it by ourclientId(cl_…) or your ownexternalClientId. 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
statusand a finerstage. Send your ownexternalIdentifier(unique across your account and sub-accounts) and key your records by ourid(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: <key> or Authorization: Bearer <key>. 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
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) orvolunteer.disclosureProvided:truemeans you gave the applicant a clear written disclosure, in a document consisting only of that disclosure, that a background check may be obtained.authorizationObtained:truemeans 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 sameexternalIdentifier, 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 inorder. forceReorder: trueorders anyway and is billed as a new check.- Always send an
externalIdentifierand reuse it when you retry after a timeout or network error. A retry never creates a second check: you get the check, or409 duplicate_checkwith it (your first attempt worked). - A
502whose body is a check (statusPENDING) means the order was sent but its answer was lost. Don't useforceReorder. It resolves on its own within about an hour and you getcheck.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 |
CLEARandREVIEWchecks carrycompletedAt,expirationDate(completion pluscheckValidityYears, default 2; set it withPUT /settingsor per sub-account withPUT /clients/{clientRef}/settings) andreportUrl, the report in the Datasource portal.- Finished checks carry
charges(US dollars). - Read one check with
GET /clients/{clientRef}/checks/{checkRef}(checkRefis ouridor yourexternalIdentifier). Cancel one that is still open withPOST .../{checkRef}/cancel. - Reconcile with
GET /clients/{clientRef}/checks?updated_since=...&limit=500, followingnextCursoruntil it is null. Do it nightly foraccountand 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 aCLEARorREVIEWcheck'sexpirationDate.ping: sent byPOST /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.
// 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 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 problem details with a stable code and a type link
to its entry in the error catalog. Branch on the HTTP status, then on
code; never on title or detail.
10. Going live
- Run your test integration end to end: an order, every status through the simulator, signature checks, a nightly reconciliation.
- Ask Datasource for your
pk_live_key and set your production webhook URL withPUT /settingsonhttps://api.datasourcecorp.app(settings are per environment). - 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 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.