API reference: Datasource Background Check Partner API
Every operation and webhook, generated from the OpenAPI document (/openapi/v1.json), the same source as the interactive reference. 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 with a stable code: see the error catalog.
Account
Check that your API key works.
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 inRetry-After. Requests with a valid key are never throttled this way.
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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
Your webhook URL, the return URLs the connect page may send a browser back to, and your webhook signing secret.
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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"webhookUrl": "https://app.example.com/cbc/webhooks",
"connectReturnUrls": [
"https://app.example.com/cbc/connect-done"
]
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 and which return URLs the connect page may send a browser back to.
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).
connectReturnUrls holds up to 10 https:// URLs. A returnUrl in POST /connect/sessions must match one
of them exactly. This call replaces both values; send the complete set.
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 | Up to 10 https:// URLs the connect page may send a browser back to. A returnUrl must match one exactly. |
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:
{
"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'thttps://, 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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"webhookUrl": "https://app.example.com/cbc/webhooks",
"connectReturnUrls": [
"https://app.example.com/cbc/connect-done"
]
}
400 example validation_failed (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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: OK400: Bad Request401: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 inRetry-After. Requests with a valid key are never throttled this way.
400 example (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"webhookSecret": "whsec_YOUR_SECRET"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 signedpingwas queued for delivery to your webhook URL.400:validation_failed: no webhook URL is registered. Register one withPUT /settingsfirst.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 inRetry-After. Requests with a valid key are never throttled this way.
202 example (application/json):
{
"status": "QUEUED"
}
400 example validation_failed (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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"
}
Connect
The hosted connect page: a church enters its own details and billing on a page we host, so you never see card data or passwords.
POST /connect/sessions
Create a connect session (operationId CreateConnectSession)
Starts the hosted connect page for one church. Call this from your server when the church's admin clicks
your "Connect" button, then send their browser to the returned connectUrl (a normal top-level
navigation; no iframe). The link works once and must be opened within 15 minutes.
The church enters its details and billing (card or invoice) on our page, so you never see card data or
passwords. When it finishes we send the browser back to your returnUrl with session_id and state
(completed, cancelled or migration_requested) and no secrets, and send a client.connected webhook.
Confirm with the webhook or GET /connect/sessions/{sessionId} before treating the church as connected.
returnUrl must exactly match one of your registered connectReturnUrls (PUT /settings).
409 if externalClientId is already an active client.
The church's primary contact gets an email from Datasource with their sign-in and a temporary password (changed at first sign-in). Submitting the page twice is safe: the second submit is told the account is being created, and only one account is ever made. Invoice billing uses the billing setup copied from your account plus the church's billing contact; a card is entered by the church on our page and never reaches you.
Request body (application/json): ConnectSessionRequest
| Field | Type | Required | Description |
|---|---|---|---|
externalClientId |
string (≤ 100 chars) |
yes | Your own id for the church (max 100 characters). It becomes the client's externalClientId. |
churchName |
string (≤ 200 chars) |
yes | The church's name (max 200 characters), shown read-only on the page. |
prefill |
object | null | ConnectPrefill |
no | Optional details to pre-fill on the contact step. |
returnUrl |
string (uri) |
yes | Where to send the browser when the church is done. Must exactly match one of your registered connectReturnUrls (see PUT /settings). We add session_id and state. |
Request example:
{
"externalClientId": "org-1042",
"churchName": "Grace Community Church",
"returnUrl": "https://app.example.com/cbc/connect-done",
"prefill": {
"contactName": "Dana Lopez",
"contactEmail": "dana@gracecommunity.example",
"contactPhone": "(303) 555-0142"
}
}
Responses:
201: The session was created. Send the church's browser toconnectUrlwithin 15 minutes; the link works once.400:validation_failed(a field is missing or too long) orinvalid_return_url(returnUrlisn't one of your registeredconnectReturnUrls; register it withPUT /settings). Nothing was created.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:client_already_connected: thisexternalClientIdis already an active client. Use it (GET /clients?externalClientId=) instead of connecting again.429:rate_limited: more than 30 sessions were created in a minute (or too many invalid-key requests came from your IP address). WaitRetry-Afterseconds (60).
201 example (application/json):
{
"sessionId": "8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60",
"connectUrl": "https://dev.partners.cbgc.io/connect?t=Zk3qL0b2m9Xr7Tn4Vw1Yc6Hs8Pd5Ja2Ue0Gi3Ko7Mf9",
"expiresAt": "2026-10-16T15:19:05Z"
}
400 example invalid_return_url (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#invalid_return_url",
"title": "Return URL not registered",
"status": 400,
"detail": "returnUrl must exactly match one of your registered connectReturnUrls. Register it with PUT /settings first.",
"code": "invalid_return_url"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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 client_already_connected (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#client_already_connected",
"title": "Client already connected",
"status": 409,
"detail": "A client with externalClientId 'org-1042' is already active under your account.",
"code": "client_already_connected"
}
429 example rate_limited (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#rate_limited",
"title": "Too many requests",
"status": 429,
"detail": "At most 30 connect sessions can be created per minute. Wait a minute and try again.",
"code": "rate_limited"
}
GET /connect/sessions/{sessionId}
Get a connect session (operationId GetConnectSession)
Returns where a connect session stands: created, opened, completed (with the new clientId),
cancelled, expired or migration_requested. Use it, or the client.connected webhook, to confirm a
church is connected after the browser returns to you.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sessionId |
path | string |
yes | The session's sessionId (a UUID), from POST /connect/sessions or the session_id on the browser's return. Only your own sessions resolve. |
Responses:
200: Where the session stands.clientIdis set oncestateiscompleted.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:session_not_found: no session with that id exists under your account.429:rate_limited: too many requests with an invalid API key from your IP address. Wait the number of seconds inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"sessionId": "8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60",
"externalClientId": "org-1042",
"state": "completed",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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 session_not_found (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#session_not_found",
"title": "Connect session not found",
"status": 404,
"detail": "No connect session '8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60' exists under your account.",
"code": "session_not_found"
}
429 example rate_limited (application/problem+json):
{
"type": "https://partners.cbgc.io/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
Your customers (churches). Each one is a client under your account, addressed by our clientId or your own externalClientId.
GET /clients
List clients (operationId ListClients)
Returns every client under your account, oldest first. Pass externalClientId to look up one client by your own id: the list then holds that client, 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 clients, oldest first. WithexternalClientId, 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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
[
{
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"externalClientId": "org-1042",
"name": "Grace Community Church",
"status": "Active",
"createdAt": "2026-10-16T15:04:05Z",
"portalUrl": "https://portal.example.com"
}
]
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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)
Provisions one of your customers as a client under your account. Each customer needs its own client before you can order checks for them.
A portal login is created for the primary contact, and Datasource emails them their username and a temporary password
(changed at first sign-in). No password is ever returned to you; welcomeEmailSent says whether the email went.
The new client starts with your account's packages and settings.
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:
{
"externalClientId": "org-1042",
"legalCompanyName": "Grace Community Church",
"dbaCompanyName": "Grace Community",
"primaryContactFirstName": "Dana",
"primaryContactLastName": "Lopez",
"primaryContactEmail": "dana@gracecommunity.example",
"primaryContactPhone": "(303) 555-0142",
"streetLine1": "1200 Larimer St",
"streetLine2": "Suite 300",
"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.welcomeEmailSentis false if that email couldn't be sent (contact support).400:validation_failed: a required field is missing or too long.errorsnames 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 thisexternalClientId. 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 inRetry-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):
{
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"externalClientId": "org-1042",
"name": "Grace Community Church",
"status": "ACTIVE",
"createdAt": "2026-10-16T15:04:05Z",
"portalUrl": "https://portal.example.com",
"primaryUsername": "dana@gracecommunity.example",
"welcomeEmailSent": true
}
400 example validation_failed (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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 thatclientIdorexternalClientIdexists 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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"externalClientId": "org-1042",
"name": "Grace Community Church",
"status": "Active",
"createdAt": "2026-10-16T15:04:05Z",
"portalUrl": "https://portal.example.com"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; anything else is 404 client_not_found. |
Responses:
200: OK401: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 Found429:rate_limited: too many requests with an invalid API key from your IP address. Wait the number of seconds inRetry-After. Requests with a valid key are never throttled this way.
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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: OK400: Bad Request401: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 Found429:rate_limited: too many requests with an invalid API key from your IP address. Wait the number of seconds inRetry-After. Requests with a valid key are never throttled this way.
400 example (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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
The product levels (1 to 6) you can order, and their base prices.
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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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 thatclientIdorexternalClientIdexists 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 inRetry-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):
[
{
"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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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. ShowfeesNotenext 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 inRetry-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):
[
{
"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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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
Order (one at a time or in batches), track, cancel and look up background checks for a client's applicants.
GET /batches/{batchId}
Get a batch (operationId GetBatch)
Returns a batch: counts of its checks by status, and one item per applicant of the original request.
status is processing while any check is QUEUED or PENDING, and completed once none is.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
batchId |
path | string |
yes | The batch's batchId (a UUID), from POST /clients/{clientRef}/checks/batch. Only your own batches resolve. |
Responses:
200: The batch: counts by status and one item per applicant.statusiscompletedonce no check isQUEUEDorPENDING.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:batch_not_found: no batch with that id exists under your account.429:rate_limited: too many requests with an invalid API key from your IP address. Wait the number of seconds inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"batchId": "3f1c0b9e-7a52-4a6e-9d0c-5e7d2b1a8c44",
"batchReference": "onboarding-2026-10-16",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "processing",
"counts": {
"QUEUED": 1,
"PENDING": 0,
"CREATED": 0
},
"items": [
{
"index": 0,
"externalIdentifier": "vol-8841",
"result": "queued",
"check": {
"id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d",
"externalIdentifier": "vol-8841",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "QUEUED",
"stage": "queued",
"level": 2,
"expirationDate": null,
"completedAt": null,
"createdAt": "2026-10-16T15:10:00Z",
"updatedAt": "2026-10-16T15:10:00Z"
}
},
{
"index": 1,
"externalIdentifier": "vol-8842",
"result": "invalid",
"errors": {
"email": [
"email is not a valid email address."
]
}
}
],
"createdAt": "2026-10-16T15:10:00Z"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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 batch_not_found (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#batch_not_found",
"title": "Batch not found",
"status": 404,
"detail": "No batch '3f1c0b9e-7a52-4a6e-9d0c-5e7d2b1a8c44' exists under your account.",
"code": "batch_not_found"
}
429 example rate_limited (application/problem+json):
{
"type": "https://partners.cbgc.io/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}/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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. FollownextCursoruntil it is null.400:invalid_cursor:cursorisn't anextCursorwe issued; start again from the first page. (invalid_json:updated_sinceorlimitisn'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 thatclientIdorexternalClientIdexists 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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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
Order a background check (operationId OrderCheck)
Orders a background check for an applicant. The applicant receives an email invitation to provide their personal details and consent directly with us, so you only send their name, email and what to order.
What to order. Send the level (1 to 6, see GET /products). We translate
the level to the client's own product; 422 Level not available means the client doesn't have that level
(contact support). orderedBy is optional: it is written to the order's notes so the client's staff can
see who asked, and is never stored by us or used to create a login.
Job location. Your CHMS must collect each applicant's job location and state of employment from the
church and send them as jobLocation and stateOfEmployment. They decide which state's rules apply, and
some levels refuse an order without them. Send only what the church tells you; never use the church's own
address or another guess.
Duplicate protection. An order is rejected with 409 Conflict (and nothing is billed) when the same
externalIdentifier, or the same applicant email for this client, already has a check that is in progress
(PENDING, CREATED) or completed and not yet expired (CLEAR, REVIEW). Checks that ended in ERROR or
CANCELLED, or have expired, can be re-ordered normally. 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. Don't retry with forceReorder; the check will update via webhook or polling, or contact support
with its id.
One externalIdentifier, one check, across all your clients. An externalIdentifier already used by a check
of another of your clients is 409 external_identifier_in_use; nothing is ordered. Reusing it for the same client
is the normal retry and re-order path.
PENDING checks resolve on their own. A check left PENDING by a lost answer is looked up with the screening
provider every 10 minutes: if the provider has it, it takes the provider's status; if the provider still doesn't
have it after an hour, it becomes ERROR (never submitted, so ordering again is allowed). Either way you get
check.updated.
Statuses. CLEAR is reported only when the provider says no search found records. Any other completed
result is REVIEW (flagged), so your users look at it in the portal.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
clientRef |
path | string |
yes | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; anything else is 404 client_not_found. |
Request body (application/json): CreateOrderRequest
| 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. |
applicantEmail |
string (email) |
yes | Applicant's email address. The invitation is sent here. Also used to detect duplicate orders for the same applicant. |
applicantPhone |
string | null |
no | Optional: the applicant's mobile number, 10-digit US or Canadian in any format (e.g. 801-555-1234). When sent, the invitation is also texted to it; the email is still sent. Never stored by us. |
firstName |
string |
yes | Applicant's first name. |
lastName |
string |
yes | Applicant's last name. |
level |
integer (int32; 1 to 6) | null |
yes | Product level to order, 1 to 6 (see GET /products). |
jobLocation |
string (≤ 100 chars) | null |
no | Where the applicant will work, e.g. Blue Springs, MO (at most 100 characters). Some levels require it with stateOfEmployment; send both or neither. Send it only as the church gives it. |
stateOfEmployment |
string | null |
no | The US state the applicant will work in (the employment jurisdiction), as a 2-letter code such as MO or a state name. Send with jobLocation. |
orderedBy |
object | null | OrderedBy |
no | Optional: who ordered this check. It is written to the order's notes so the client's staff can see who asked. Never stored by us and never creates a login. |
forceReorder |
boolean |
no | Order even if the applicant already has an in-progress or unexpired check. Defaults to false. A forced re-order is billed as a new check. |
Request example:
{
"externalIdentifier": "vol-8841",
"firstName": "Jordan",
"lastName": "Kim",
"applicantEmail": "jordan.kim@example.com",
"applicantPhone": "801-555-1234",
"level": 2,
"jobLocation": "Blue Springs, MO",
"stateOfEmployment": "MO",
"orderedBy": {
"email": "dana@gracecommunity.example",
"firstName": "Dana",
"lastName": "Lopez"
},
"forceReorder": false
}
Responses:
201: The check was submitted (statusCREATED) and the applicant has been emailed. Billable.Locationpoints at the check.400:validation_failed: a field is missing or invalid (for example nolevel). 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.404:client_not_found: no client with thatclientIdorexternalClientIdexists under your account. Don't retry.409: Nothing was ordered or billed. Two cases, told apart bycode:duplicate_check(this body: the applicant already has a check in progress or unexpired, inorder), orexternal_identifier_in_use(problem details: theexternalIdentifierbelongs to a check of another of your clients; use a different one).422:level_not_available: this client has no product for that level. A configuration problem on our side; 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 inRetry-After. Requests with a valid key are never throttled this way.502: Two cases. If the body is a check (statusPENDING, nocode), the submission's answer was lost: the check may exist upstream, is keptPENDING, and is resolved automatically within about an hour (check.updatedfollows). Retry only with the sameexternalIdentifier, never withforceReorder. If the body is problem details withupstream_unavailable, nothing was created or recorded, and a retry is safe.
201 example (application/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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"
}
409 example duplicate_check (application/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": "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"
}
}
422 example level_not_available (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d",
"externalIdentifier": "vol-8841",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "PENDING",
"stage": "submitting",
"level": 2,
"expirationDate": null,
"completedAt": null,
"createdAt": "2026-10-16T15:10:00Z",
"updatedAt": "2026-10-16T15:10:00Z"
}
POST /clients/{clientRef}/checks/batch
Order checks for a list of people (operationId OrderBatch)
Orders checks for up to 500 people in one call. Send the level, an optional orderedBy, an optional
batchReference, and the applicants.
Every applicant is validated and duplicate-checked first, against your stored checks and against the
applicants before them in the same request. Each gets a result: queued, duplicate (with the
existing check) or invalid (with errors). A problem with one applicant never fails the batch;
only a problem with the batch itself (no applicants, more than 500, or a missing or invalid level) is a
400.
The response is 202 Accepted with a batchId. Queued checks are submitted in the background, a few at
a time; each sends its own check.updated webhook as it moves from QUEUED to PENDING to CREATED,
and batch.completed is sent once none is waiting. Poll GET /batches/{batchId} if you prefer.
Send a batchReference to make the call safe to retry: sending the same value again returns the first
batch with 200 OK and orders nothing new. A client can have one running batch at a time (409).
An applicant whose externalIdentifier belongs to a check of another of your clients comes back as an invalid
item (errors.externalIdentifier); nothing is ordered for it.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
clientRef |
path | string |
yes | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; anything else is 404 client_not_found. |
Request body (application/json): BatchOrderRequest
| Field | Type | Required | Description |
|---|---|---|---|
level |
integer (int32; 1 to 6) | null |
yes | Product level to order for everyone, 1 to 6 (see GET /products). |
jobLocation |
string (≤ 100 chars) | null |
no | Where the applicants will work, e.g. Blue Springs, MO (at most 100 characters), for everyone without their own. Some levels require it with stateOfEmployment; send both or neither, and only as the church gives it. |
stateOfEmployment |
string | null |
no | The US state the applicants will work in (2-letter code or name), with jobLocation. |
orderedBy |
object | null | OrderedBy |
no | Optional: who ordered these checks. Written to each order's notes only; never stored and never creates a login. |
batchReference |
string (≤ 100 chars) | null |
no | Your idempotency key for this batch (max 100 characters). Sending the same value again returns the first batch instead of ordering again. |
applicants |
BatchApplicant[] (≥ 1 items; ≤ 500 items) |
yes | The people to order checks for, 1 to 500. |
Request example:
{
"level": 2,
"batchReference": "onboarding-2026-10-16",
"orderedBy": {
"email": "dana@gracecommunity.example",
"firstName": "Dana",
"lastName": "Lopez"
},
"applicants": [
{
"firstName": "Jordan",
"lastName": "Kim",
"email": "jordan.kim@example.com",
"externalIdentifier": "vol-8841"
},
{
"firstName": "Sam",
"lastName": "Ortiz",
"email": "sam.ortiz@example",
"externalIdentifier": "vol-8842"
}
]
}
Responses:
200: A batch with thisbatchReferencealready exists: this is that first batch, and nothing new was ordered.202: A new batch was accepted. Queued checks are submitted in the background; see each item'sresult. Billable per submitted check.400:validation_failed: a problem with the batch itself: no applicants, more than 500, a missing or invalidlevel, or a field too long. Problems with single applicants never fail the batch; they come back asinvaliditems.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 thatclientIdorexternalClientIdexists under your account. Don't retry.409:batch_in_progress: this client already has a batch that is still running. Nothing was ordered. Send the batch again onceGET /batches/{batchId}shows the other onecompleted.422:level_not_available: this client has no product for that level. Nothing was ordered; contact support.429:rate_limited: too many requests with an invalid API key from your IP address. Wait the number of seconds inRetry-After. Requests with a valid key are never throttled this way.502:upstream_unavailable: the screening provider didn't answer. This happens before anything is stored (looking up the level's product), so nothing was ordered and a retry is safe.
200 example (application/json):
{
"batchId": "3f1c0b9e-7a52-4a6e-9d0c-5e7d2b1a8c44",
"batchReference": "onboarding-2026-10-16",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "processing",
"counts": {
"QUEUED": 1,
"PENDING": 0,
"CREATED": 0
},
"items": [
{
"index": 0,
"externalIdentifier": "vol-8841",
"result": "queued",
"check": {
"id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d",
"externalIdentifier": "vol-8841",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "QUEUED",
"stage": "queued",
"level": 2,
"expirationDate": null,
"completedAt": null,
"createdAt": "2026-10-16T15:10:00Z",
"updatedAt": "2026-10-16T15:10:00Z"
}
},
{
"index": 1,
"externalIdentifier": "vol-8842",
"result": "invalid",
"errors": {
"email": [
"email is not a valid email address."
]
}
}
],
"createdAt": "2026-10-16T15:10:00Z"
}
202 example (application/json):
{
"batchId": "3f1c0b9e-7a52-4a6e-9d0c-5e7d2b1a8c44",
"batchReference": "onboarding-2026-10-16",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "processing",
"counts": {
"QUEUED": 1,
"PENDING": 0,
"CREATED": 0
},
"items": [
{
"index": 0,
"externalIdentifier": "vol-8841",
"result": "queued",
"check": {
"id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d",
"externalIdentifier": "vol-8841",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "QUEUED",
"stage": "queued",
"level": 2,
"expirationDate": null,
"completedAt": null,
"createdAt": "2026-10-16T15:10:00Z",
"updatedAt": "2026-10-16T15:10:00Z"
}
},
{
"index": 1,
"externalIdentifier": "vol-8842",
"result": "invalid",
"errors": {
"email": [
"email is not a valid email address."
]
}
}
],
"createdAt": "2026-10-16T15:10:00Z"
}
400 example validation_failed (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#validation_failed",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"applicants": [
"applicants can hold at most 500 applicants per batch."
]
},
"code": "validation_failed"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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"
}
409 example batch_in_progress (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#batch_in_progress",
"title": "Batch in progress",
"status": 409,
"detail": "This client already has a batch that is still running. Wait for it to complete (GET /batches/{batchId}) before sending another.",
"code": "batch_in_progress"
}
422 example level_not_available (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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"
}
POST /clients/{clientRef}/checks/lookup
Find existing checks by email (operationId LookupChecks)
Finds the latest check for each of up to 500 applicant emails, including history imported when a church was
moved to you. Each result is keyed by the email's position in your request (index): emails are never
returned. Use it to match your own people to existing checks, then link them with
PATCH /clients/{clientRef}/checks/{checkRef}.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
clientRef |
path | string |
yes | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; anything else is 404 client_not_found. |
Request body (application/json): CheckLookupRequest
| Field | Type | Required | Description |
|---|---|---|---|
emails |
string (email)[] (≥ 1 items; ≤ 500 items) |
yes | 1 to 500 applicant email addresses. Matching ignores case and surrounding spaces. The response never contains an email. |
Request example:
{
"emails": [
"jordan.kim@example.com",
"sam.ortiz@example.com"
]
}
Responses:
200: One item per email, in request order, keyed byindex.checkis the latest check for that email at this client, or null. Emails are never returned.400:validation_failed:emailsis empty, has more than 500 entries, or an entry is longer than 254 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: no client with thatclientIdorexternalClientIdexists 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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"items": [
{
"index": 0,
"check": {
"id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d",
"externalIdentifier": null,
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "CLEAR",
"stage": "complete",
"level": null,
"expirationDate": "2028-01-07T18:22:41Z",
"completedAt": "2026-01-07T18:22:41Z",
"createdAt": "2026-01-05T15:10:00Z",
"updatedAt": "2026-01-07T18:22:41Z"
}
},
{
"index": 1,
"check": null
}
]
}
400 example validation_failed (application/problem+json):
{
"type": "https://partners.cbgc.io/docs/errors.md#validation_failed",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"emails": [
"emails can hold at most 500 addresses."
]
},
"code": "validation_failed"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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}/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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_foundorcheck_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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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:
{
"externalIdentifier": "vol-8841"
}
Responses:
200: The check, now with yourexternalIdentifier.400:validation_failed:externalIdentifieris 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_foundorcheck_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 anexternalIdentifier(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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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 nowCANCELLED. IfupstreamCancelledis 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_foundorcheck_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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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}/remind
Re-send the applicant's invitation (operationId RemindApplicant)
Sends the applicant their invitation again: by email, and by text if applicantPhone was sent with the
order. Only for a check waiting for the applicant (CREATED, stage awaiting_applicant), at most once an
hour. checkRef may be the check's id (chk_…) or your externalIdentifier.
409 check_not_awaiting_applicant if the applicant already finished (or the check is closed);
409 reminder_too_soon within an hour of the last reminder.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
clientRef |
path | string |
yes | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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: OK401: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 Found409: Conflict429:rate_limited: too many requests with an invalid API key from your IP address. Wait the number of seconds inRetry-After. Requests with a valid key are never throttled this way.502: Bad Gateway
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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"
}
409 example (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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 (application/problem+json):
{
"type": "https://partners.cbgc.io/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"
}
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 | The client: our clientId (cl_…) or your own externalClientId (at most 100 characters; URL-encode it). Both resolve only among your own clients; 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 newcheck.updateddelivery with the check's current state was queued (a neweventId).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_foundorcheck_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 inRetry-After. Requests with a valid key are never throttled this way.
200 example (application/json):
{
"message": "Webhook replay request accepted",
"id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d",
"status": "QUEUED"
}
401 example invalid_api_key (application/problem+json):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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):
{
"type": "https://partners.cbgc.io/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.
// 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", ""))
// 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), awaiting_applicant (invite sent; the applicant hasn't submitted), applicant_ready (the applicant submitted, and the church must review and release the order in its portal, e.g. a possible duplicate or a jurisdiction question), in_progress (the applicant submitted; searches are running), complete (final result ready: CLEAR or REVIEW), closed (will not complete: ERROR or CANCELLED). awaiting_applicant, applicant_ready and in_progress are all 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 church'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 church's Datasource portal (needs the church's 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 the church's 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:
{
"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 batch.completed: Batch completed
Sent once when no check of a batch is QUEUED or PENDING any more: every check has been submitted (or released). It is not a results event; results come as check.updated. Same signature, retry and eventId rules as check.updated.
| Field | Type | Required | Description |
|---|---|---|---|
event |
string (one of "batch.completed") |
no | Always batch.completed. |
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). |
batchId |
string (uuid) |
no | The batch, as returned by POST /clients/{clientRef}/checks/batch. |
batchReference |
string | null |
no | Your batchReference for the batch, if you sent one. |
clientId |
string |
no | The client the batch was ordered for (its clientId). |
externalClientId |
string | null |
no | Your own id for the client, if it has one. |
counts |
object |
no | Number of checks per status at completion, e.g. { "CREATED": 12 }. |
Example:
{
"event": "batch.completed",
"eventId": "0b8f2e6d-4c1a-4f3e-9d7b-5a2c8e1f6d30",
"timestamp": "2026-10-16T15:12:30Z",
"batchId": "3f1c0b9e-7a52-4a6e-9d0c-5e7d2b1a8c44",
"batchReference": "onboarding-2026-10-16",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"externalClientId": "org-1042",
"counts": {
"QUEUED": 0,
"PENDING": 0,
"CREATED": 1
}
}
Webhook client.connected: Church connected
Sent when a church finishes the hosted connect page and its client exists (status is Active). Confirm a connection with this event or GET /connect/sessions/{sessionId} before treating the church as connected; never with the browser redirect alone. Same signature, retry and eventId rules as check.updated.
| Field | Type | Required | Description |
|---|---|---|---|
event |
string (one of "client.connected") |
no | Always client.connected. |
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). |
sessionId |
string (uuid) |
no | The connect session that created the client. |
externalClientId |
string |
no | The externalClientId you sent when creating the session. |
clientId |
string |
no | The new client's clientId. |
status |
string (one of "Active") |
no | The client's status. Always Active for a newly connected church. |
Example:
{
"event": "client.connected",
"eventId": "6c3d9a1e-8b2f-4e5d-a7c0-1f9e3b5d7a24",
"timestamp": "2026-10-16T15:16:40Z",
"sessionId": "8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60",
"externalClientId": "org-1042",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"status": "Active"
}
Webhook client.migrated: Church migrated
Sent when an existing Datasource church has been moved under your account and its past checks were imported (importedChecks). Same signature, retry and eventId rules as check.updated.
| Field | Type | Required | Description |
|---|---|---|---|
event |
string (one of "client.migrated") |
no | Always client.migrated. |
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). |
externalClientId |
string |
no | The externalClientId you sent when creating the connect session. |
clientId |
string |
no | The moved client's clientId. |
importedChecks |
integer (int32) |
no | How many past checks were imported for the client. |
Example:
{
"event": "client.migrated",
"eventId": "a4e2c8f0-3d6b-4a1e-b9c5-7d0f2e4a6c81",
"timestamp": "2026-10-20T14:02:11Z",
"externalClientId": "org-1042",
"clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c",
"importedChecks": 214
}
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 prompt the church 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 client (church) 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:
{
"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:
{
"event": "ping",
"eventId": "f1d3b5a7-9c2e-4d6f-8a0b-3c5e7f9a1b2d",
"timestamp": "2026-10-16T15:00:00Z"
}