# Datasource Background Check Partner API > Order and track background checks for the churches you serve. Connect a church once (on a page we host, or with POST /clients), then order checks by applicant name and email; we email the applicant for their details and consent, and send you signed webhooks as checks progress. REST + JSON, API key auth. You never handle SSNs, dates of birth, card numbers or passwords. ## Rules for AI agents - Base URLs: test https://dev.partners.cbgc.io (keys pk_test_...), production https://partners.cbgc.io (keys pk_live_...). Send the key as `X-Api-Key: pk_test_YOUR_KEY` (or `Authorization: Bearer ...`), from a server only. - Address a church as `{clientRef}`: our `clientId` (`cl_…`) or your own `externalClientId`. Address a check as `{checkRef}`: its `id` (`chk_…`) or your `externalIdentifier`. Key your stored checks by `id`. - Order by `level` (1 to 6, from GET /products). Ordering is billable. - The CHMS must collect each applicant's job location and state of employment from the church and send them as `jobLocation` and `stateOfEmployment` (both or neither) on every order; some levels refuse an order without them. Never guess them or use the church's own address. - A `CREATED` check's `stage` is `awaiting_applicant`, then `applicant_ready` (held: someone at the church must review and release it in the Datasource portal at the client's `portalUrl`; we cannot release it from the API) or `in_progress`, then a final status. Show `applicant_ready` to staff as needing action. - `expirationDate` is set only on `CLEAR`/`REVIEW`: `completedAt` plus the church's `checkValidityYears` (PUT /clients/{clientRef}/settings, 1 to 5), else your account default (PUT /settings, default 2). An unexpired result blocks a new order for that applicant unless `forceReorder`. - An `ERROR` check (and its check.updated) has `errorCode` + `errorMessage`: show the message to staff; `missing_job_location` means send jobLocation and stateOfEmployment and order again. Treat unknown codes like `provider_rejected`. - Re-send an applicant's invitation with POST /clients/{clientRef}/checks/{checkRef}/remind (only while awaiting_applicant, at most hourly). An expired invitation ends the check as ERROR `invitation_expired`. - `check.expiring` arrives 30 and 7 days before a CLEAR/REVIEW check's expirationDate. CLEAR/REVIEW checks carry `reportUrl` (portal link; needs the church's sign-in); finished checks carry `charges` (USD). - GET /settings/webhook-deliveries shows the last 30 days of delivery attempts to your webhook endpoint. - Optional `applicantPhone` (batch: `applicants[].phone`), a 10-digit US or Canadian mobile number: the invitation is also texted to it. - Always send your own `externalIdentifier` (≤ 100 characters, unique across all your churches) and reuse it on every retry: a retry never creates a second check. A `409 duplicate_check` on a retry means the first attempt worked: use `order` from the body. - Never set `forceReorder` after a `502`. A 502 whose body is a check (`status` PENDING) resolves on its own within about an hour; a 502 with `code` `upstream_unavailable` created nothing and is safe to retry. - Errors are RFC 9457 problem details with a stable `code`; branch on HTTP status, then `code`. Never on `title` or `detail`. - Verify `X-Partner-Signature` on every webhook: lowercase hex HMAC-SHA256 of the raw body, keyed with the whole `whsec_...` secret, compared in constant time. Answer 2xx within 15 seconds. - Webhooks are at least once and unordered: drop repeats by `eventId`; for each check keep the state with the latest `timestamp`. - Connect a church with POST /connect/sessions (server side), redirect the browser to `connectUrl`, and confirm with the `client.connected` webhook or GET /connect/sessions/{sessionId}. Never treat the return redirect alone as proof. - Register your webhook URL and every connect `returnUrl` with PUT /settings first (https only). - Run a nightly reconciliation per church: GET /clients/{clientRef}/checks?updated_since=...&limit=500, following `nextCursor` until null. - Show prices with their `feesNote` ("Plus pass-through fees."). - Ignore unknown response fields, enum values and webhook events. ## Docs - [Integration guide](https://partners.cbgc.io/docs/guide.md): concepts, every flow end to end, code samples, limits - [Error catalog](https://partners.cbgc.io/docs/errors.md): every `code`, its HTTP status, and whether a retry is safe - [Changelog](https://partners.cbgc.io/docs/changelog.md): dated API changes - [OpenAPI 3.1](https://partners.cbgc.io/openapi/v1.json): machine-readable reference with examples - [Interactive reference](https://partners.cbgc.io/scalar) - [Everything in one file](https://partners.cbgc.io/llms-full.txt): this file, the guide, the error catalog, a section per operation and per webhook with full JSON examples The test environment serves the same documents at https://dev.partners.cbgc.io, plus its test-environment routes. --- # Datasource Background Check Partner API: Integration Guide Order and track background checks for the churches you serve, from your own platform. Every chapter below stands on its own; links point at the exact chapter that has more. The interactive reference (every route, field and enum) is at `/scalar`, the machine-readable OpenAPI 3.1 document at `/openapi/v1.json`, the error catalog at `/docs/errors.md`, and everything in one file for AI coding agents at `/llms-full.txt`. ## 1. Start here **What the API does.** You connect a church once, either on a page we host (it enters its own details and billing) or with one API call. Then you order background checks for its volunteers and staff by name and email. We email each applicant to collect their details and consent, and send you signed webhooks as each check moves along. You never handle SSNs, dates of birth, card numbers or passwords. | Environment | Base URL | API key prefix | Checks | |---|---|---|---| | Test (sandbox) | `https://dev.partners.cbgc.io` | `pk_test_` | Not real; nothing is charged. Checks can be moved to any status ([chapter 11](#11-testing-in-the-sandbox)). | | Production | `https://partners.cbgc.io` | `pk_live_` | Real and billed to the church. | **Getting keys.** Sandbox keys are self-serve: sign up at https://join.cbgc.io/sandbox/, confirm your email, and you get a test API key (`pk_test_...`) and a webhook signing secret (`whsec_...`), each shown once. A person from our team follows up to help. Production keys come from Datasource, through an expiring single-view link, once your integration works end to end in the sandbox. Store keys and secrets in a secret manager, never in browser or mobile app code. **First check in ten minutes** (test environment; replace `pk_test_YOUR_KEY`): 1. Check your key: ```bash curl https://dev.partners.cbgc.io/status -H "X-Api-Key: pk_test_YOUR_KEY" ``` 2. Register your webhook URL and connect return URL ([chapter 9](#9-webhooks)): ```bash curl -X PUT https://dev.partners.cbgc.io/settings -H "X-Api-Key: pk_test_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"webhookUrl":"https://app.example.com/cbc/webhooks","connectReturnUrls":["https://app.example.com/cbc/connect-done"]}' ``` 3. Connect a church ([chapter 4](#4-connecting-a-church)): `POST /connect/sessions`, then open the returned `connectUrl` in a browser and fill in the page (choose **invoice** to finish without a card). 4. See what you can order: `GET /products` ([chapter 5](#5-products-and-levels)). 5. Order a check ([chapter 6](#6-ordering-a-check)): ```bash curl -X POST https://dev.partners.cbgc.io/clients/org-1042/checks -H "X-Api-Key: pk_test_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"externalIdentifier":"vol-8841","firstName":"Jordan","lastName":"Kim","applicantEmail":"jordan.kim@example.com","level":2}' ``` Then move it to a result ([chapter 11](#11-testing-in-the-sandbox)) and watch your webhook receive `check.updated`. ## 2. Concepts and glossary ``` You (partner, one API key) └── Client = one church {clientRef}: clientId or your externalClientId └── Check = one applicant's background check externalIdentifier (yours), id ``` | Term | What it is | Format | Example | Who creates it | |---|---|---|---|---| | Partner | You. Your API key identifies you; you only ever see your own data. | n/a | n/a | Datasource | | Client | One church you serve, with its own billing and portal logins at Datasource. | object | Grace Community Church | You (connect page or `POST /clients`) | | `clientId` | Our id for a client. | string, `cl_` + 24 characters | `cl_5b0e6f2a8c1d4e7b9a3f2d6c` | Datasource | | `externalClientId` | Your id for a client. Unique among your clients. | string, ≤ 100 characters | `org-1042` | You | | `{clientRef}` | Any path segment that names a client: `clientId` or `externalClientId`. | string | `org-1042` | You | | Check | One background check for one applicant. | object | check `chk_9c2e4f6a1b3d4e5f8a7b6c5d` | You (an order) | | `id` | Our id for a check, set when it is ordered. The reliable key for a check. | string, `chk_` + 24 characters | `chk_9c2e4f6a1b3d4e5f8a7b6c5d` | Datasource | | `externalIdentifier` | Your id for a check. Unique across **all** your clients; reuse it on retries. | string, ≤ 100 characters | `vol-8841` | You | | `{checkRef}` (path) | Any path segment that names a check: `id` or `externalIdentifier`. | string | `vol-8841` | You | | `level` | A product, 1 to 6. We translate it to each church's own product. | integer 1 to 6 | `2` | Datasource defines; you choose | | `status` | Where a check is; contract-stable. | enum (7 values) | `CREATED` | Datasource | | `stage` | A finer view of `status`. | enum (7 values) | `awaiting_applicant` | Datasource | | Batch | Up to 500 checks ordered in one call. | object | `batchId` `3f1c0b9e-...` | You | | `batchReference` | Your idempotency key for a batch. | string, ≤ 100 characters | `onboarding-2026-10-16` | You | | Connect session | One run of the hosted connect page for one church. | object | `sessionId` `8d2f6c14-...` | You | | Event | One webhook delivery's content. | object | `check.updated` | Datasource | | `eventId` | Unique id of an event, the same on every retry of it. Your dedupe key. | UUID string | `e7a1c3b5-2d4f-4a6b-8c9d-0e1f2a3b4c5d` | Datasource | **Conventions.** JSON in and out, camelCase names. All timestamps are UTC, ISO 8601 (for example `2026-10-16T15:10:00Z`). Prices are in USD. URL-encode ids you put in a path. Ignore fields and webhook events you don't know: we add them without notice ([chapter 15](#15-versioning-and-changes)). ## 3. Authentication Send your API key on every request, in either header: ```http X-Api-Key: pk_test_YOUR_KEY Authorization: Bearer pk_test_YOUR_KEY ``` - A test key works only on `https://dev.partners.cbgc.io` and a live key only on `https://partners.cbgc.io`. - **Rotation without downtime.** You can hold more than one active key. Ask Datasource for a new key, deploy it, then ask us to revoke the old one. `GET /status` answers `200` for any working key. - `401 invalid_api_key`: the key is missing, invalid, revoked, or for the other environment. Don't retry until it's fixed. - `429 rate_limited`: more than 20 requests a minute with an invalid key came from your IP address. Wait the `Retry-After` seconds (60). Requests with a valid key are never throttled this way. - Keep keys on your server. Never put them in a browser, mobile app, or a URL. ## 4. Connecting a church A church enters its own details and billing (card or invoice) on a page we host, so you never see card data or passwords. One run of the page is a **connect session**. **Before the first session**, register your webhook URL and your return URLs with `PUT /settings` ([chapter 9](#9-webhooks)). **The flow:** 1. When the church's admin clicks your "Connect to Datasource" button, your **server** calls `POST /connect/sessions`. 2. You redirect the admin's browser to the returned `connectUrl` (a normal top-level navigation; no iframe). The link works once and must be opened within 15 minutes. 3. The church fills in four steps: church details, primary contact, billing (card or invoice), and the service agreement. If it chooses **Already a Datasource customer?**, it sends a migration request instead ([chapter 13](#13-existing-datasource-churches)). 4. We create the church's account and email its primary contact a sign-in with a temporary password (changed at first sign-in). The email comes from Datasource; you never receive passwords. 5. We send the browser to `{returnUrl}?session_id={sessionId}&state={state}` and send you `client.connected`. 6. **Confirm** with the `client.connected` webhook or `GET /connect/sessions/{sessionId}` before treating the church as connected. The redirect carries no secrets and can be forged. **Create a session.** `POST /connect/sessions`: | Field | Type | Required | Limits / format | Example | Meaning | |---|---|---|---|---|---| | `externalClientId` | string | yes | ≤ 100 characters | `org-1042` | Your id for the church; becomes the client's `externalClientId`. | | `churchName` | string | yes | ≤ 200 characters | `Grace Community Church` | Shown read-only on the page. | | `returnUrl` | string | yes | URI; must exactly match a registered `connectReturnUrls` entry | `https://app.example.com/cbc/connect-done` | Where the browser goes when done. We append `session_id` and `state`. | | `prefill.contactName` | string | no | ≤ 200 characters | `Dana Lopez` | Pre-fills the contact step; the church can change it. | | `prefill.contactEmail` | string | no | ≤ 200 characters | `dana@gracecommunity.example` | Pre-fills the contact's email, which becomes their sign-in. | | `prefill.contactPhone` | string | no | ≤ 200 characters | `(303) 555-0142` | Pre-fills the contact's phone. | ```bash curl -X POST https://dev.partners.cbgc.io/connect/sessions \ -H "X-Api-Key: pk_test_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"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"}}' ``` ```js // Node 20+, no libraries const BASE = "https://dev.partners.cbgc.io"; const API_KEY = process.env.DATASOURCE_API_KEY; // pk_test_YOUR_KEY const res = await fetch(`${BASE}/connect/sessions`, { method: "POST", headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ externalClientId: "org-1042", churchName: "Grace Community Church", returnUrl: "https://app.example.com/cbc/connect-done", prefill: { contactName: "Dana Lopez", contactEmail: "dana@gracecommunity.example" }, }), }); if (res.status !== 201) throw new Error(`connect session failed: ${res.status} ${(await res.json()).code}`); const session = await res.json(); // Store session.sessionId with your church record BEFORE redirecting: the webhook can arrive before the browser does. // Then redirect the admin's browser to session.connectUrl. ``` `201 Created`: ```json { "sessionId": "8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60", "connectUrl": "https://dev.partners.cbgc.io/connect?t=Zk3qL0b2m9Xr7Tn4Vw1Yc6Hs8Pd5Ja2Ue0Gi3Ko7Mf9", "expiresAt": "2026-10-16T15:19:05Z" } ``` Errors: `400 validation_failed` (a field missing or too long), `400 invalid_return_url` (not registered), `409 client_already_connected` (that `externalClientId` is already an active client), `429 rate_limited` (more than 30 sessions a minute; `Retry-After: 60`). Creating a session isn't idempotent: an unused session simply expires. **The return.** `state` in the redirect is `completed`, `cancelled` or `migration_requested`. If the session expires or is locked, the browser sees a page on our side and is **not** sent back; poll `GET /connect/sessions/{sessionId}` and offer to start again. **Confirm.** `GET /connect/sessions/{sessionId}`: ```json { "sessionId": "8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60", "externalClientId": "org-1042", "state": "completed", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c" } ``` | `state` | Meaning | |---|---| | `created` | Waiting for the church to open the link. | | `opened` | The church is on the page (including while its account is being created). | | `completed` | The client exists; `clientId` is set. | | `cancelled` | The church cancelled. | | `expired` | The link wasn't opened within 15 minutes, an hour passed after opening, or the session was locked after too many failed attempts (10 failed submits, or 3 failed billing attempts). Create a new session. | | `migration_requested` | The church says it's already a Datasource customer; we verify it by hand ([chapter 13](#13-existing-datasource-churches)). | **What the church sees.** A used link shows "This link was already used"; an expired one "This link has expired". Both ask the church to start again from your app. Submitting the page twice (a double click, a second tab) is safe: the second submit is told its account is being created, and only one account is ever made. A failed card is shown on the billing step to try again; no account exists until billing works. For **invoice**, the church's account takes the billing setup copied from your account, plus the church's billing contact. **Creating a client yourself.** If the church's details are already in your platform, `POST /clients` creates the client directly. Required: `legalCompanyName`, `primaryContactFirstName`, `primaryContactLastName`, `primaryContactEmail`, `primaryContactPhone`, `streetLine1`, `city`, `state`, `postalCode`. Optional: `externalClientId` (≤ 100 characters, unique among your clients; `409 external_client_id_in_use` if taken), `dbaCompanyName`, `companyPhone`, `streetLine2`, `country` (default `US`), and the `billing...` address fields. The church's primary contact gets a portal login and an email from Datasource with their username and a temporary password (changed at first sign-in), the same as on the connect page: no password is ever returned to you. The response includes `primaryUsername` (their email, or a derived name such as `dana-3f9a1c` if that email is already a login) and `welcomeEmailSent`. Look a client up with `GET /clients?externalClientId=org-1042` or `GET /clients/{clientRef}`; `portalUrl` is where the church's staff sign in (open it in a new tab). ## 5. Products and levels You order a **level** from 1 to 6; we translate it to the church's own product. `GET /products` lists every level with its base price from your account; `GET /clients/{clientRef}/products` lists the levels one church can order, priced for it (levels it doesn't have are left out). Always show `feesNote` next to a price: third-party court and county fees are added per check. | Field | Type | Example | Meaning | |---|---|---|---| | `level` | integer, 1 to 6 | `2` | Send it as `level` when ordering. | | `name` | string | `Standard` | Product name. | | `description` | string | `National criminal database, county search, SSN trace.` | What it covers. | | `price` | number (USD) or null | `34.95` | Base price; null if not available. Prices can differ per church. | | `feesNote` | string | `Plus pass-through fees.` | Always this text. | ```bash curl https://dev.partners.cbgc.io/clients/org-1042/products -H "X-Api-Key: pk_test_YOUR_KEY" ``` ```json [ { "level": 1, "name": "Basic", "description": "National criminal database.", "price": 19.95, "feesNote": "Plus pass-through fees." }, { "level": 2, "name": "Standard", "description": "National criminal database, county search, SSN trace.", "price": 34.95, "feesNote": "Plus pass-through fees." } ] ``` `502 upstream_unavailable` on either route: safe to retry. ## 6. Ordering a check `POST /clients/{clientRef}/checks` orders one check. **It is billable once submitted.** We email the applicant to collect their details and consent. | Field | Type | Required | Limits / format | Example | Meaning | |---|---|---|---|---|---| | `firstName` | string | yes | | `Jordan` | Applicant's first name. | | `lastName` | string | yes | | `Kim` | Applicant's last name. | | `applicantEmail` | string | yes | email | `jordan.kim@example.com` | The invitation goes here; also used to find duplicates (case-insensitive). | | `applicantPhone` | string | no | 10-digit US or Canadian mobile number, any format | `801-555-1234` | When sent, the invitation is also texted here. Never stored by us. | | `level` | integer | yes | 1 to 6 | `2` | What to order ([chapter 5](#5-products-and-levels)). | | `externalIdentifier` | string | strongly recommended | ≤ 100 characters; unique across all your clients | `vol-8841` | Your id for the check. Reuse it on retries. | | `orderedBy.email` / `.firstName` / `.lastName` | string | no | ≤ 200 characters each | `dana@gracecommunity.example` | Who asked. Written to the order's notes in the church's portal only; never stored by us and never creates a login. | | `jobLocation`, `stateOfEmployment` | string | when the level requires them | ≤ 100 characters; a US state code or name | `Blue Springs, MO`, `MO` | Where the applicant will work, as the church tells you. Some levels require both; send both or neither. We pass them on unchanged and never fill them in. | | `forceReorder` | boolean | no | default `false` | `false` | Order even if a duplicate exists. Billed as a new check. | > **Your CHMS must collect each applicant's job location and state of employment.** Ask the church, for every person it > screens, where that person will work (`jobLocation`, e.g. `Blue Springs, MO`) and in which state > (`stateOfEmployment`), and send both with the order. They decide which state's screening rules apply, and some levels > refuse an order without them. Use only what the church tells you: never fill them in from the church's own address or > any other guess, since a wrong jurisdiction puts false information on a consumer report order. Batches take the same > pair ([chapter 7](#7-ordering-in-bulk)). ```bash curl -X POST https://dev.partners.cbgc.io/clients/org-1042/checks \ -H "X-Api-Key: pk_test_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"externalIdentifier":"vol-8841","firstName":"Jordan","lastName":"Kim","applicantEmail":"jordan.kim@example.com", "level":2,"orderedBy":{"email":"dana@gracecommunity.example","firstName":"Dana","lastName":"Lopez"}}' ``` ```js // Node 20+ async function orderCheck(clientRef, person) { const res = await fetch(`${BASE}/clients/${encodeURIComponent(clientRef)}/checks`, { method: "POST", headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ externalIdentifier: person.id, // your id: reuse it on every retry firstName: person.firstName, lastName: person.lastName, applicantEmail: person.email, level: 2, }), }); const body = await res.json(); if (res.status === 201) return body; // the new check if (res.status === 409 && body.code === "duplicate_check") return body.order; // it already exists if (res.status === 502 && !body.code) return body; // PENDING: resolves on its own, don't force throw new Error(`order failed: ${res.status} ${body.code}`); } ``` `201 Created` (with a `Location` header): ```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" } ``` **Duplicate rules.** Before ordering we look for an existing check with the same `externalIdentifier` (across all your clients), or the same applicant email at this client: | Existing check | Result | |---|---| | `QUEUED`, `PENDING` or `CREATED` | `409 duplicate_check`, existing check in `order`; nothing billed | | `CLEAR` or `REVIEW`, not yet expired | `409 duplicate_check`; nothing billed | | `CLEAR` or `REVIEW`, expired | Ordered again (same check `id`, new order) | | `ERROR` or `CANCELLED` | Ordered again (same check `id`, new order) | | Same `externalIdentifier` on a check of **another** of your clients | `409 external_identifier_in_use`; nothing ordered. Use a different id. | | None | Ordered | `forceReorder: true` skips the first two rows and is billed as a new check. A re-order reuses the check's row: same `id`, and `status` goes back to `PENDING` then `CREATED`. **Other outcomes.** - `422 level_not_available`: the church has no product for that level. Contact support. - `502` with a **check** body (`status` `PENDING`, no `code`): the order was sent but its answer was lost. Don't retry with `forceReorder`. A `PENDING` check is resolved automatically within about an hour ([chapter 8](#8-tracking-status)), and you get `check.updated`. Retrying with the same `externalIdentifier` is always safe. - `502 upstream_unavailable` (problem details): nothing was created; retry. - A check has its `id` from the moment it is ordered, even while `QUEUED` or `PENDING`. **Other check routes.** `GET /clients/{clientRef}/checks/{checkRef}` reads one check. `POST .../{checkRef}/cancel` cancels it (cancel before the applicant submits to avoid charges; `upstreamCancelled: false` means the provider didn't confirm, so contact support). `POST .../{checkRef}/replay-webhook` queues a new `check.updated` with the current state. ## 7. Ordering in bulk `POST /clients/{clientRef}/checks/batch` orders up to 500 checks in one call. **Each submitted check is billable.** | Field | Type | Required | Limits / format | Example | Meaning | |---|---|---|---|---|---| | `level` | integer | yes | 1 to 6 | `2` | What to order for everyone. | | `batchReference` | string | recommended | ≤ 100 characters | `onboarding-2026-10-16` | Idempotency key: the same value again returns the first batch. | | `orderedBy` | object | no | each field ≤ 200 characters | | As for single orders. | | `applicants` | array | yes | 1 to 500 items | | The people. | | `applicants[].firstName`, `.lastName` | string | yes | | `Jordan`, `Kim` | Applicant's name. | | `applicants[].email` | string | yes | valid email, ≤ 254 characters | `jordan.kim@example.com` | The invitation goes here. | | `applicants[].phone` | string | no | as `applicantPhone` | `801-555-1234` | When sent, the invitation is also texted here. An invalid number makes the item `invalid`. | | `applicants[].externalIdentifier` | string | recommended | ≤ 100 characters, unique across your clients | `vol-8841` | Your id for that check. | | `jobLocation`, `stateOfEmployment` | string | when the level requires them | as for single orders | `Blue Springs, MO`, `MO` | Where everyone works, for applicants without their own. | | `applicants[].jobLocation`, `.stateOfEmployment` | string | no | as for single orders | `Overland Park, KS`, `KS` | This applicant's own; replaces the batch's pair. Half a pair makes the item `invalid`. | ```bash curl -X POST https://dev.partners.cbgc.io/clients/org-1042/checks/batch \ -H "X-Api-Key: pk_test_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"level":2,"batchReference":"onboarding-2026-10-16", "applicants":[{"firstName":"Jordan","lastName":"Kim","email":"jordan.kim@example.com","externalIdentifier":"vol-8841"}, {"firstName":"Sam","lastName":"Ortiz","email":"sam.ortiz@example","externalIdentifier":"vol-8842"}]}' ``` ```js // Node 20+ const res = await fetch(`${BASE}/clients/org-1042/checks/batch`, { method: "POST", headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ level: 2, batchReference: "onboarding-2026-10-16", applicants }), }); const batch = await res.json(); // 202: new batch, 200: the batch you already sent with this batchReference for (const item of batch.items) { if (item.result === "invalid") console.warn(item.index, item.errors); } ``` `202 Accepted` (or `200 OK` for a repeated `batchReference`): ```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", "status": "QUEUED", "stage": "queued" } }, { "index": 1, "externalIdentifier": "vol-8842", "result": "invalid", "errors": { "email": ["email is not a valid email address."] } } ], "createdAt": "2026-10-16T15:10:00Z" } ``` (`check` is the full check; shortened here.) | `result` | Meaning | Billed? | |---|---|---| | `queued` | Accepted; will be submitted in the background. `check` is the new (or re-ordered) check. | Yes, once submitted | | `duplicate` | The applicant already has a current check (same rules as [chapter 6](#6-ordering-a-check)), or appears earlier in this batch. `check` is that check. | No | | `invalid` | The applicant's data was rejected, or its `externalIdentifier` belongs to another client's check. See `errors`. | No | - Problems with single applicants never fail the batch. The batch itself is `400 validation_failed` only for: no applicants, more than 500, a missing or invalid `level`, or a batch field too long. - `409 batch_in_progress`: the client already has a running batch. Send yours once that one is `completed`. - `422 level_not_available` and `502 upstream_unavailable`: nothing was ordered; see [chapter 10](#10-errors). - Queued checks are submitted four at a time. Each sends `check.updated` as it moves `QUEUED` → `PENDING` → `CREATED`. - `batch.completed` fires once no check is `QUEUED` or `PENDING`: everything is submitted, not finished. Results come as `check.updated`. - `GET /batches/{batchId}` returns the same shape. `status` is `processing` or `completed`; `counts` always has `QUEUED`, `PENDING` and `CREATED`, plus any other status present. ## 8. Tracking status Each check has a contract-stable `status` and a finer `stage`: | `status` | Final? | `stage` | Meaning | |---|---|---|---| | `QUEUED` | no | `queued` | In a batch, not yet sent. | | `PENDING` | no | `submitting` | Being sent, or sent with its answer lost (resolved automatically, see below). | | `CREATED` | no | `awaiting_applicant` | Submitted; the applicant has been invited and hasn't finished the form yet. | | `CREATED` | no | `applicant_ready` | The applicant finished the form, but the order is held for the church to review and release in its portal (the portal's **Applicant Ready** queue). Usually a possible duplicate or a jurisdiction question. **The church has to act**; see below. | | `CREATED` | no | `in_progress` | The applicant finished the form (and, if it was held, the church released it); the searches are running. | | `CLEAR` | yes | `complete` | Completed, and no search found records (nor did the church's decision, if it uses one, fail the applicant). | | `REVIEW` | yes | `complete` | Completed and **flagged**: a search found records, or the church's decision didn't pass. The church reviews it in its portal. Also used if the result couldn't be read, so a check is never shown clear by mistake. | | `ERROR` | yes | `closed` | Could not be completed, or never reached the screening provider. `errorCode` and `errorMessage` say why (below). Ordering again is allowed. | | `CANCELLED` | yes | `closed` | Cancelled. Ordering again is allowed. | Every move between `CREATED` stages sends `check.updated`. A stage never goes back to `awaiting_applicant` once the applicant has submitted. ### The life of a check 1. **You order it.** `POST /clients/{clientRef}/checks`: the check is `PENDING` (`submitting`) for a moment, then `CREATED` (`awaiting_applicant`). The applicant gets an email from Datasource, and a text too if you sent `applicantPhone`, with a link to a secure form. 2. **The applicant fills in the form.** They give their details (date of birth, Social Security number, addresses) and consent on Datasource's form, never in your app. The invitation link expires after a set number of days. 3. **The order is either released automatically or held.** - Most orders go straight to the searches: the stage becomes `in_progress`. - Some are held in the church's **Applicant Ready** queue: the stage becomes `applicant_ready`. Usually this is a possible duplicate (the applicant already has a check) or a jurisdiction question. Someone at the church signs in to its Datasource portal (`portalUrl` on the client), reviews the order, may choose which counties to search (this can change the price), and releases it. We cannot release it from the API, because it is the church's decision and can change what it pays. Once released, the stage becomes `in_progress`. 4. **The searches run.** This can take from minutes to several days, depending on the searches and the courts. The check stays `CREATED` (`in_progress`). 5. **The result.** The check becomes `CLEAR` (no search found records) or `REVIEW` (something was found, or the church's decision didn't pass), stage `complete`. The report itself stays in the church's portal; only the outcome is in the API. **Show `applicant_ready` to the church's staff.** Nothing moves until someone at the church releases the order, so show it as needing action, e.g. "Action needed: review and release this check in your Datasource portal", with a link to the client's `portalUrl`. `CLEAR` is reported only when the screening provider says no search found records; any other completed result is `REVIEW` (flagged), so a person always looks at it. Results themselves stay in the church's portal; they are not in the API. `completedAt` is set when a check reaches a final status: the screening provider's own completion time when it gives one. `expirationDate` is set for `CLEAR` and `REVIEW` only: `completedAt` plus the client's `checkValidityYears` (1 to 5): its own (`PUT /clients/{clientRef}/settings`), else your account's default (`PUT /settings`, default 2). Until then, ordering the same applicant again is refused as a duplicate unless you send `forceReorder`. **Per-church validity.** Each church can keep results for its own number of years: ```bash curl -X PUT https://dev.partners.cbgc.io/clients/org-1042/settings -H "X-Api-Key: pk_test_YOUR_KEY" \ -H "Content-Type: application/json" -d '{"checkValidityYears": 3}' ``` The answer has the church's own value (`checkValidityYears`, null when it uses your default) and the one that applies (`effectiveCheckValidityYears`). Send `null` to go back to your account's default. `GET /clients/{clientRef}/settings` reads it. A change applies to checks completed after it. **The report, and what it cost.** A `CLEAR` or `REVIEW` check has `reportUrl`: the report in the church's Datasource portal. It opens only after the church's own sign-in, so it is safe to show to staff, but it's no use to anyone else. A finished check also has `charges`: what the screening provider charged for it, in US dollars (`0` for free test personas). Both are null until then. **Re-sending the invitation.** Applicants lose emails. `POST /clients/{clientRef}/checks/{checkRef}/remind` sends the invitation again (and the text, if you sent `applicantPhone`), for a check waiting for the applicant (`CREATED`, stage `awaiting_applicant`), at most once an hour (`409 reminder_too_soon` otherwise; `409 check_not_awaiting_applicant` once the applicant has finished). An invitation that expires before the applicant finishes closes the check as `ERROR` (`invitation_expired`); order again to send a new one. **Expiry reminders.** 30 days and again 7 days before a `CLEAR` or `REVIEW` check's `expirationDate` you get `check.expiring` (chapter 9), so you can prompt the church to re-screen before the clearance lapses. **Why a check failed.** An `ERROR` check, and its `check.updated`, carry `errorCode` (stable, for your code) and `errorMessage` (one sentence to show the church's staff). Both are null for every other status, and ordering again clears them. | `errorCode` | Meaning | What to do | |---|---|---| | `missing_job_location` | The level needs the applicant's job location and state of employment. | Send `jobLocation` and `stateOfEmployment`, order again. | | `missing_information` | The screening provider needs other details; `errorMessage` names them. | Add them, order again. | | `invalid_applicant_details` | The applicant's phone number or email was refused. | Fix `applicantPhone` or `applicantEmail`, order again. | | `provider_rejected` | The screening provider refused the order for another reason. | Check the applicant's details; contact support with the check's `id`. | | `not_submitted` | The order never reached the screening provider. Nothing was charged. | Order again. | | `provider_error` | The screening provider couldn't complete the check. | Contact support with the check's `id`. | | `invitation_expired` | The applicant didn't finish the form before the invitation expired. The order was cancelled. | Order again to send a new invitation. | New codes may be added; treat an unknown one like `provider_rejected`. We never pass on the screening provider's own error text, which can contain the applicant's details. **Allowed transitions:** | From | To | When | |---|---|---| | (new) | `QUEUED` | Accepted in a batch. | | (new) | `PENDING` → `CREATED` | A single order is submitted. | | `QUEUED` | `PENDING`, then `CREATED` | The batch submits it. | | `QUEUED` | `ERROR` | It couldn't be sent. | | `PENDING` | `CREATED`, `CLEAR`, `REVIEW`, `ERROR`, `CANCELLED` | The provider confirms it (live, or found by the automatic lookup). | | `PENDING` | `ERROR` | After about an hour, the provider has no such order: it was never submitted. | | `CREATED` | `CLEAR`, `REVIEW`, `ERROR`, `CANCELLED` | The check finishes, or is cancelled. | | any | `CANCELLED` | You cancel it. | | `ERROR`, `CANCELLED`, or expired `CLEAR`/`REVIEW` | `PENDING` or `QUEUED` | You order again for the same applicant (same `id`). | **`PENDING` resolves on its own.** Every 10 minutes we look up checks that have been `PENDING` for 15 minutes or more with the screening provider. Found: the check gets its real status. Not found after an hour: it becomes `ERROR`. Either way you get `check.updated`, and the check's batch can complete. **Webhooks or polling.** Use webhooks for speed ([chapter 9](#9-webhooks)) and a nightly reconciliation for certainty. Both give the same check objects. **Reconciliation recipe** (run nightly, per connected church): 1. Keep `lastSync` per church (start with the church's connect time). 2. `GET /clients/{clientRef}/checks?updated_since={lastSync}&limit=500`. 3. For each item, upsert by `id`, keeping the newer `updatedAt`. 4. While `nextCursor` isn't null, repeat with `&cursor={nextCursor}`. A check can appear on two pages; upserting by `id` handles it. 5. Set `lastSync` to the time you started step 2. ```js // Node 20+ async function reconcile(clientRef, since) { let cursor = null; do { const url = new URL(`${BASE}/clients/${encodeURIComponent(clientRef)}/checks`); url.searchParams.set("updated_since", since); url.searchParams.set("limit", "500"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: { "X-Api-Key": API_KEY } }); if (!res.ok) throw new Error(`reconcile failed: ${res.status}`); const page = await res.json(); for (const check of page.items) await upsertCheck(check); // your storage, keyed by check.id cursor = page.nextCursor; } while (cursor); } ``` ```bash curl "https://dev.partners.cbgc.io/clients/org-1042/checks?updated_since=2026-10-15T00:00:00Z&limit=500" -H "X-Api-Key: pk_test_YOUR_KEY" ``` `limit` defaults to 100 and is at most 500. `updated_since` without an offset is UTC. A bad `cursor` is `400 invalid_cursor`. ## 9. Webhooks **Register** with `PUT /settings`. It replaces both URL values, so send the full set: | Field | Type | Required | Limits / format | Example | Meaning | |---|---|---|---|---|---| | `webhookUrl` | string or null | no | `https://` URI, ≤ 2048 characters, host must resolve only to public addresses | `https://app.example.com/cbc/webhooks` | Where events go. Empty or omitted turns webhooks off. | | `connectReturnUrls` | string array | no | ≤ 10 entries, each an `https://` URI | `["https://app.example.com/cbc/connect-done"]` | Allowed `returnUrl`s. Omitted clears them. | | `checkValidityYears` | integer | no | 1 to 5; default 2 | `2` | Your account's default for how many years a `CLEAR` or `REVIEW` check stays valid (its `expirationDate` is `completedAt` plus this), for churches without their own (`PUT /clients/{clientRef}/settings`). Omitted keeps the current value. A change applies to checks completed after it. | **Delivery log.** `GET /settings/webhook-deliveries` lists every attempt to deliver a webhook to you in the last 30 days, newest first: the event, its `eventId`, the check, your endpoint's HTTP status, how long it took, and which attempt it was. Filter with `eventId` or `checkId`; `limit` is 1 to 200 (default 50). Bodies are never stored, ours or yours; to get a check's current state again, use `replay-webhook`. `GET /settings` reads them back. `POST /settings/webhook-secret` rotates the signing secret: the new one is returned once and the old one stops working immediately, so rotate when quiet. `POST /settings/webhook-test` sends a signed `ping` (`400 validation_failed` if no webhook URL is registered). **Events.** Every event is a `POST` with `Content-Type: application/json`, a flat JSON object with `event`, `eventId` and `timestamp`, signed in `X-Partner-Signature`. | `event` | When | Extra fields | |---|---|---| | `check.updated` | A check changed status (or you called replay-webhook) | the check: `id`, `externalIdentifier`, `clientId`, `status`, `stage`, `level`, `expirationDate`, `completedAt`, `reportUrl`, `charges`, `errorCode`, `errorMessage` | | `check.expiring` | A `CLEAR` or `REVIEW` check expires in 30 days, then again in 7 (each once) | `id`, `externalIdentifier`, `clientId`, `status`, `expirationDate`, `daysLeft` | | `batch.completed` | No check of a batch is `QUEUED` or `PENDING` | `batchId`, `batchReference`, `clientId`, `externalClientId`, `counts` | | `client.connected` | A church finished the connect page | `sessionId`, `externalClientId`, `clientId`, `status` (`Active`) | | `client.migrated` | An existing Datasource church was moved under you | `externalClientId`, `clientId`, `importedChecks` | | `ping` | You called `POST /settings/webhook-test` | none | ```json { "event": "check.updated", "eventId": "e7a1c3b5-2d4f-4a6b-8c9d-0e1f2a3b4c5d", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "stage": "complete", "level": 2, "expirationDate": "2028-10-18T18:22:41Z", "completedAt": "2026-10-18T18:22:41Z", "timestamp": "2026-10-18T18:22:42Z" } ``` **Verify every delivery** before trusting it. The signature is the lowercase hex HMAC-SHA256 of the **raw** request body (the exact bytes, before parsing), keyed with the UTF-8 bytes of your whole webhook secret (`whsec_` prefix included). Compare in constant time. ```js // Node 20+, no libraries import http from "node:http"; import crypto from "node:crypto"; const SECRET = process.env.DATASOURCE_WEBHOOK_SECRET; // whsec_YOUR_SECRET function isValidSignature(rawBody, header) { const expected = Buffer.from(crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex"), "utf8"); const given = Buffer.from(header ?? "", "utf8"); return given.length === expected.length && crypto.timingSafeEqual(given, expected); } http.createServer((req, res) => { const chunks = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => { const raw = Buffer.concat(chunks); if (!isValidSignature(raw, req.headers["x-partner-signature"])) { res.writeHead(401).end(); return; } const event = JSON.parse(raw.toString("utf8")); res.writeHead(200).end(); // answer fast... queueForProcessing(event); // ...then work asynchronously; skip it if event.eventId was already seen }); }).listen(3000); ``` ```python # Python 3 import hashlib, hmac, os def is_valid_signature(raw_body: bytes, header: str | None) -> bool: expected = hmac.new(os.environ["DATASOURCE_WEBHOOK_SECRET"].encode("utf-8"), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header or "") ``` ```csharp // C# using System.Security.Cryptography; using System.Text; static bool IsValidSignature(byte[] rawBody, string? header, string secret) { var expected = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), rawBody)).ToLowerInvariant(); return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(header ?? "")); } ``` **Delivery rules.** - Any `2xx` within 15 seconds is success. Anything else, a timeout, or a connection error is retried up to 5 more times with increasing delays (seconds at first, then minutes). Redirects are not followed. After the last retry the event is dropped; your reconciliation ([chapter 8](#8-tracking-status)) or `POST .../{checkRef}/replay-webhook` covers it. - Delivery is **at least once**: drop repeats by `eventId`. A retry resends the identical body (same `eventId` and `timestamp`). A replay is a new event with a new `eventId`. - Delivery is **not ordered**: for each check (key by `id`) keep the state with the latest `timestamp`, or re-read the check with `GET /clients/{clientRef}/checks/{checkRef}`. - The signature covers only the body. For replay protection, also reject events whose `timestamp` is far in the past (for example more than a day). - Answer `401` for a bad signature; it will be retried, which is harmless. - Key checks by `id`: every check has one from the moment it is ordered. ## 10. Errors Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details (`application/problem+json`) with a stable `code` and a `type` link to its entry in [the error catalog](https://partners.cbgc.io/docs/errors.md). Branch on the HTTP status, then `code`. ```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" } ``` Two bodies aren't problem details: `409 duplicate_check` on ordering (the existing check is in `order`) and `502` on ordering when the body is the `PENDING` check. | `code` | HTTP | Retry the same request? | What to do | |---|---|---|---| | `validation_failed` | 400 | No | Fix each field named in `errors`. | | `invalid_cursor` | 400 | No | Start again from the first page. | | `invalid_return_url` | 400 | No | Register the URL with `PUT /settings`. | | `invalid_json` | 400 | No | Send valid JSON with values of the right type. | | `invalid_api_key` | 401 | No | Use a valid key for this environment. | | `client_not_found`, `check_not_found`, `batch_not_found`, `session_not_found` | 404 | No | Check the id; only your own objects resolve. | | `not_found` / `method_not_allowed` | 404 / 405 | No | Check the path and method. | | `duplicate_check` | 409 | No | Use the check in `order`; `forceReorder` only for a new billed check. | | `external_client_id_in_use` | 409 | No | Use the existing client. | | `client_already_connected` | 409 | No | The church is already connected. | | `external_identifier_in_use` | 409 | No | Use a different `externalIdentifier`. | | `batch_in_progress` | 409 | Yes, later | Wait for the running batch to complete. | | `check_not_awaiting_applicant` | 409 | No | Only a check waiting for the applicant can be re-sent. | | `reminder_too_soon` | 409 | Yes, later | Wait an hour after the last reminder. | | `payload_too_large` / `unsupported_media_type` | 413 / 415 | No | Send less / send `application/json`. | | `level_not_available` | 422 | No | Pick an available level or contact support. | | `rate_limited` | 429 | Yes, after `Retry-After` | Wait, then retry. | | `internal_error` | 500 | Yes, same `externalIdentifier` | Retry with backoff. Quote the `traceId` to support. | | `upstream_unavailable` | 502 | Usually | Retry with backoff; for orders see [chapter 6](#6-ordering-a-check). | Every entry, with details: [/docs/errors.md](https://partners.cbgc.io/docs/errors.md). ## 11. Testing in the sandbox The test environment (`https://dev.partners.cbgc.io`, `pk_test_` keys) runs every flow against a sandbox: nothing is charged and no real check is run. Applicants don't finish on their own there, so the test environment has an extra route that moves a check to any status. It exists only in the test environment, and the test environment's copy of this guide ([https://dev.partners.cbgc.io/docs/guide.md](https://dev.partners.cbgc.io/docs/guide.md)) describes it. **Test plan** (run it end to end in the test environment before asking for a production key): 1. `GET /status` answers `200`; a wrong key answers `401 invalid_api_key`. 2. `PUT /settings`, then `POST /settings/webhook-test`: your endpoint gets a `ping` and verifies its signature. 3. Connect a church through the page (choose invoice). Confirm with `client.connected` and `GET /connect/sessions/{id}`. 4. Start a second session and cancel it on the page: the browser comes back with `state=cancelled`. 5. Order a check; then order it again with the same `externalIdentifier`: the second is `409 duplicate_check`. 6. Move three checks to `CLEAR`, `REVIEW` and `ERROR` (test environment route, above); your app shows each and drops a repeated `eventId`. 7. Order a batch of three with one bad email: one `invalid` item, and `batch.completed` once the others are `CREATED`. 8. Send the same `batchReference` again: `200` with the same `batchId`. 9. Run your reconciliation job: it finds every check from steps 5 to 7. ## 12. Going live - [ ] Your webhook endpoint verifies `X-Partner-Signature`, answers within 15 seconds, and drops repeated `eventId`s. - [ ] You branch on HTTP status and `code`, never on `title` or `detail`. - [ ] Every order sends an `externalIdentifier` and retries reuse it; a `502` on ordering never triggers `forceReorder`. - [ ] You confirm connections with `client.connected` or `GET /connect/sessions/{sessionId}`, never the redirect alone. - [ ] A nightly reconciliation job runs for every connected church. - [ ] Prices are shown with `feesNote`. - [ ] You have your production key (`pk_live_...`) and webhook secret from Datasource, stored in a secret manager. - [ ] On production: `PUT /settings` with your production webhook and return URLs, then `POST /settings/webhook-test`. ## 13. Existing Datasource churches A church that already uses Datasource can choose **Already a Datasource customer?** on the connect page. Its browser comes back with `state=migration_requested`, and `GET /connect/sessions/{sessionId}` shows `migration_requested`. We verify the request by hand with the contact we already have on file, then move the church's account under yours. It keeps its logins, billing and reports. When the move and the import of its past checks are done, you get `client.migrated` with `externalClientId`, `clientId` and `importedChecks`. From then on the church works like any other client. Until that event, treat the church as not yet connected. **Linking past checks to your people.** Imported checks have no `externalIdentifier`. Find them by email: `POST /clients/{clientRef}/checks/lookup` with `{ "emails": [...] }` (1 to 500 addresses, each ≤ 254 characters) returns the latest check per email, keyed by the email's position (`index`); emails are never returned. Then set your id once with `PATCH /clients/{clientRef}/checks/{checkRef}` and `{ "externalIdentifier": "vol-8841" }` (`409 external_identifier_in_use` if the check already has one or another check uses it). Ordering again for a person with a current imported check is `409 duplicate_check`, as for any check. ## 14. Limits | What | Limit | |---|---| | `externalIdentifier`, `externalClientId`, `batchReference` | ≤ 100 characters | | `churchName`, each `prefill` field, each `orderedBy` field | ≤ 200 characters | | Batch applicants | 1 to 500 per batch | | Batch applicant email | valid address, ≤ 254 characters | | Running batches | 1 per client at a time (a batch counts as running for at most 24 hours) | | Batch submission | 4 checks at a time | | Lookup emails | 1 to 500 per call, each ≤ 254 characters | | `GET .../checks` page size | default 100, maximum 500 | | `connectReturnUrls` | ≤ 10 URLs, `https://` only | | `webhookUrl` | `https://`, ≤ 2048 characters, public addresses only | | Connect link | opened within 15 minutes, once | | Connect session | ends 60 minutes after opening; locked after 10 failed submits or 3 failed billing attempts | | Connect sessions created | 30 per minute per partner (`429`, `Retry-After: 60`) | | Invalid API key | 20 per minute per IP address, then `429` for 60 seconds | | Webhook response time | 15 seconds | | Webhook retries | up to 5 more attempts | | `PENDING` resolution | checked after 15 minutes, every 10 minutes; released as `ERROR` after 1 hour | | Connect sessions kept | 7 days after they end | ## 15. Versioning and changes - Changes are additive: new routes, new fields, new enum values and new webhook events can appear at any time. Ignore what you don't know, and don't fail on an unknown `status`, `stage`, `state` or `event`. - A breaking change gets **90 days' notice** in [the changelog](https://partners.cbgc.io/docs/changelog.md) and by email. - `operationId`s in `/openapi/v1.json` are stable; generate a client from it if you like. ## 16. Compliance Datasource is the consumer reporting agency. The church is the end user: it makes, and is responsible for, every decision based on a report, including adverse action, which it handles in its portal. Applicants give their details and consent directly to Datasource; you never handle SSNs or dates of birth, and you never see card data. --- # Error catalog Every error response is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details (`Content-Type: application/problem+json`) with a stable `code`. Its `type` is `https://partners.cbgc.io/docs/errors.md#`, a link to the entry below. Branch on `code`: titles and details are for people and may change. ```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" } ``` Validation errors (`validation_failed`, `invalid_cursor`) also carry `errors`: an object from field name to a list of messages, for example `{ "level": ["level is required."] }`. Messages never repeat the values you sent. Two documented responses are not problem details: - `409` on `POST /clients/{clientRef}/checks` with `code` `duplicate_check`: the body is `{ code, message, currentStatus, instructions, order }`, with the existing check in `order`. - `502` on `POST /clients/{clientRef}/checks` when the body is a check (`status` `PENDING` or `ERROR`, no `code`). See [upstream_unavailable](#upstream_unavailable). Read the HTTP status first, then `code` when the body has one. ## All codes | `code` | HTTP | Retry the same request? | What to do | |---|---|---|---| | `validation_failed` | 400 | No | Fix each field named in `errors`, then send again. | | `invalid_cursor` | 400 | No | Start again from the first page (omit `cursor`). | | `invalid_return_url` | 400 | No | Register the URL with `PUT /settings`, then create the session again. | | `invalid_json` | 400 | No | Send a valid JSON body with `Content-Type: application/json`, and values of the right type. | | `invalid_api_key` | 401 | No | Use a valid key for this environment (`pk_test_` on test, `pk_live_` on production). | | `client_not_found` | 404 | No | Check the `clientGuid` or `externalClientId`; only your own clients resolve. | | `check_not_found` | 404 | No | Check the `orderId`, `externalIdentifier` or `id`, and the client. | | `batch_not_found` | 404 | No | Check the `batchId`; only your own batches resolve. | | `session_not_found` | 404 | No | Check the `sessionId`; only your own sessions resolve. | | `not_found` | 404 | No | The route doesn't exist. Check the method and path. | | `method_not_allowed` | 405 | No | Use the method the reference lists for this path. | | `duplicate_check` | 409 | No | Use the existing check in `order`. Only send `forceReorder: true` if you want a new, billed check. | | `external_client_id_in_use` | 409 | No | Use the existing client: `GET /clients?externalClientId=...`. | | `client_already_connected` | 409 | No | The church is already connected; use its client instead of a new session. | | `external_identifier_in_use` | 409 | No | Use a different `externalIdentifier` (it is unique across all your clients). | | `batch_in_progress` | 409 | Yes, later | Wait until the client's running batch is `completed`, then send again. | | `check_not_awaiting_applicant` | 409 | No | Only a check waiting for the applicant can be re-sent. | | `reminder_too_soon` | 409 | Yes, later | Wait an hour after the last reminder. | | `payload_too_large` | 413 | No | Send a smaller body (for example fewer applicants per batch). | | `unsupported_media_type` | 415 | No | Send `Content-Type: application/json`. | | `level_not_available` | 422 | No | Pick a level from `GET /clients/{clientRef}/products`, or contact support. | | `rate_limited` | 429 | Yes, after `Retry-After` | Wait the number of seconds in `Retry-After`, then send again. | | `internal_error` | 500 | Yes, with the same `externalIdentifier` | Retry with backoff; contact support if it persists. | | `upstream_unavailable` | 502 | Usually; see the entry | Retry with backoff. For orders, read the entry first. | | `request_failed` | other | Depends | Any other framework error. Read `status` and `detail`. | ## validation_failed **400.** The request is invalid: a required field is missing, a value is too long, a URL isn't `https://`, `level` is missing, and so on. `errors` lists each problem by field name (nested fields as `orderedBy.email`, list entries as `connectReturnUrls[0]`). Nothing was created or changed. **Retry:** only after fixing the request. ## invalid_cursor **400.** On `GET /clients/{clientRef}/checks`, `cursor` is not a `nextCursor` we issued. `errors.cursor` explains it. **Retry:** no; start again from the first page. ## invalid_return_url **400.** On `POST /connect/sessions`, `returnUrl` doesn't exactly match one of your registered `connectReturnUrls` (matching is exact: scheme, host, path and query). Nothing was created. **Retry:** after registering it with `PUT /settings`. ## invalid_json **400.** The body isn't valid JSON, is missing, or a value has the wrong type (for example `"level": "two"`), or a query value can't be read (for example `limit=abc` or an `updated_since` that isn't ISO 8601). Nothing was created. **Retry:** after fixing the request. ## invalid_api_key **401.** The API key is missing, invalid or revoked, or it belongs to the other environment. **Retry:** only with a valid key. Repeated invalid keys from one IP address lead to `rate_limited`. ## client_not_found **404.** No client with that `clientGuid` or `externalClientId` exists under your account. Clients of other partners always look like this. **Retry:** no. ## check_not_found **404.** No check with that `orderId`, `externalIdentifier` or numeric `id` exists for the client (or, on routes without a client, under your account). **Retry:** no. ## batch_not_found **404.** No batch with that `batchId` exists under your account. **Retry:** no. ## session_not_found **404.** No connect session with that `sessionId` exists under your account. Sessions are deleted 7 days after they end. **Retry:** no. ## not_found **404.** No such route. Check the method, the path, and that you're calling the right environment (some routes exist only in the test environment). **Retry:** no. ## method_not_allowed **405.** The path exists but not with this HTTP method. **Retry:** no. ## duplicate_check **409.** On `POST /clients/{clientRef}/checks`: the applicant (same `externalIdentifier`, or same email at this client) already has a check that is in progress (`QUEUED`, `PENDING`, `CREATED`) or completed and not yet expired (`CLEAR`, `REVIEW`). Nothing was ordered or billed. The body is not problem details: ```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" } } ``` (`order` is the full check; shortened here.) If you were retrying after a timeout, this means your first attempt worked: use `order`. **Retry:** no. `forceReorder: true` orders anyway and is billed as a new check. ## external_client_id_in_use **409.** On `POST /clients`: another of your clients already has this `externalClientId`. Nothing was created. **Retry:** no; use the existing client. ## client_already_connected **409.** On `POST /connect/sessions`: a client with this `externalClientId` is already active under your account. **Retry:** no; look it up with `GET /clients?externalClientId=...`. ## external_identifier_in_use **409.** The `externalIdentifier` is taken. On `POST /clients/{clientRef}/checks`: it belongs to a check of another of your clients (an `externalIdentifier` is unique across all your clients); nothing was ordered. On `PATCH /clients/{clientRef}/checks/{orderId}`: the check already has one (it can be set once), or another check uses the value; nothing changed. In a batch the same situation is an `invalid` item, not this error. **Retry:** no; use a different value. ## batch_in_progress **409.** On `POST /clients/{clientRef}/checks/batch`: this client already has a batch that is still running (a batch counts as running for at most 24 hours). Nothing was ordered. **Retry:** yes, once `GET /batches/{batchId}` shows the other batch `completed`. Sending the same `batchReference` as a running batch is not this error: it returns that batch. ## check_not_awaiting_applicant **409.** On `POST /clients/{clientRef}/checks/{checkRef}/remind`: the check isn't waiting for the applicant (status `CREATED`, stage `awaiting_applicant`). The applicant already finished, or the check is closed. Nothing was sent. **Retry:** no. ## reminder_too_soon **409.** On `POST /clients/{clientRef}/checks/{checkRef}/remind`: the applicant was re-sent the invitation less than an hour ago (`detail` says when). Nothing was sent. **Retry:** yes, an hour after the last reminder. ## payload_too_large **413.** The request body is larger than the server accepts. **Retry:** no; send less (a batch holds at most 500 applicants). ## unsupported_media_type **415.** The body isn't sent as `Content-Type: application/json`. **Retry:** no; set the header. ## level_not_available **422.** The client has no product for that level. This is a configuration problem on our side. Nothing was ordered. **Retry:** no; pick a level from `GET /clients/{clientRef}/products`, or contact support. ## rate_limited **429.** Either too many requests with an invalid API key came from your IP address (20 a minute; valid keys are never throttled this way), or more than 30 connect sessions were created in a minute. `Retry-After` gives the seconds to wait (an integer, 60 for both). **Retry:** yes, after waiting. ## internal_error **500.** Something failed on our side. The body is a problem details object with `code: internal_error` and a `traceId` to quote to support; it never contains error details. For an order, the check may or may not exist: retry only with the same `externalIdentifier`, and expect either a normal result or `409 duplicate_check`. **Retry:** yes, with backoff; contact support with the time of the request if it keeps happening. ## upstream_unavailable **502.** The screening provider didn't accept the request. As problem details, nothing was created or recorded and a retry is safe: this is the case for clients, products and batches, and for an order when the church's account couldn't be prepared for ordering. **For `POST /clients/{clientRef}/checks` the body can instead be the check itself** (`status` `PENDING`, no `code`): the order was sent but its answer was lost, so it may or may not exist upstream. Don't retry with `forceReorder`. Retry only with the same `externalIdentifier` (you'll get the check or `409 duplicate_check`), or just wait: a `PENDING` check is looked up with the provider automatically and resolved within about an hour, and you get `check.updated` either way (its real status if the provider has it, or `ERROR` if it never arrived, after which ordering again is allowed). **If the body is a check with `status` `ERROR`**, the screening provider refused that order outright (for example a product that needs fields this API doesn't collect). Nothing was created or billed, and you can order again once the cause is fixed; contact support if it persists. ## request_failed **Any other status** the framework produces (rare). Read `status` and `detail`. **Retry:** depends on the status. --- # Endpoint reference One section per operation, in the reference's tag order, generated from `/openapi/v1.json`. Every operation needs your API key (`X-Api-Key: pk_test_YOUR_KEY` or `Authorization: Bearer pk_test_YOUR_KEY`). Error bodies are problem details; see the error catalog above. ## Account ### GET /status **Check your API key** (operationId `CheckApiKey`) Returns `200` with a short message when the API key is valid. Useful for verifying setup and key rotation. Responses: - **`200`**: The key is valid. The body is plain text. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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 ### GET /settings **Get your settings** (operationId `GetSettings`) Returns the webhook URL and the connect return URLs you registered, and how many years a completed check stays valid. Responses: - **`200`**: Your registered webhook URL and connect return URLs. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "webhookUrl": "https://app.example.com/cbc/webhooks", "connectReturnUrls": [ "https://app.example.com/cbc/connect-done" ] } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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: ```json { "webhookUrl": "https://app.example.com/cbc/webhooks", "connectReturnUrls": [ "https://app.example.com/cbc/connect-done" ] } ``` Responses: - **`200`**: The saved settings. Both values were replaced with what you sent. - **`400`**: `validation_failed`: a URL isn't `https://`, has credentials or a fragment, or its host resolves to a private address, or there are more than 10 return URLs. Nothing was changed. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "webhookUrl": "https://app.example.com/cbc/webhooks", "connectReturnUrls": [ "https://app.example.com/cbc/connect-done" ] } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`**: OK - **`400`**: Bad Request - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `400` example (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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 in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "webhookSecret": "whsec_YOUR_SECRET" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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 signed `ping` was queued for delivery to your webhook URL. - **`400`**: `validation_failed`: no webhook URL is registered. Register one with `PUT /settings` first. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `202` example (`application/json`): ```json { "status": "QUEUED" } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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 ### 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: ```json { "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 to `connectUrl` within 15 minutes; the link works once. - **`400`**: `validation_failed` (a field is missing or too long) or `invalid_return_url` (`returnUrl` isn't one of your registered `connectReturnUrls`; register it with `PUT /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`: this `externalClientId` is 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). Wait `Retry-After` seconds (60). `201` example (`application/json`): ```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`): ```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`): ```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`): ```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`): ```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. `clientId` is set once `state` is `completed`. - **`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 in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "sessionId": "8d2f6c14-3b7a-4e0e-9c55-1a9b7d3e2f60", "externalClientId": "org-1042", "state": "completed", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```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`): ```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`): ```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 ### 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. With `externalClientId`, a list of 0 or 1 items. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json [ { "clientId": "cl_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`): ```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`): ```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: ```json { "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. `welcomeEmailSent` is false if that email couldn't be sent (contact support). - **`400`**: `validation_failed`: a required field is missing or too long. `errors` names each field. Nothing was created; fix the request and send it again. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`409`**: `external_client_id_in_use`: another of your clients already has this `externalClientId`. Nothing was created; use the existing client (`GET /clients?externalClientId=`). - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: `upstream_unavailable`: the screening provider didn't answer. Nothing was recorded on our side; it's safe to retry. `201` example (`application/json`): ```json { "clientId": "cl_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`): ```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`): ```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`): ```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`): ```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`): ```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 that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "clientId": "cl_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`): ```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`): ```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`): ```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`**: OK - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: Not Found - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`**: OK - **`400`**: Bad Request - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: Not Found - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `400` example (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`): ```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 ### 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 that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: `upstream_unavailable`: the screening provider didn't answer. Safe to retry. `200` example (`application/json`): ```json [ { "level": 2, "name": "Standard", "description": "National criminal database, county search, SSN trace.", "price": 34.95, "feesNote": "Plus pass-through fees." } ] ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`): ```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. Show `feesNote` next to prices. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: `upstream_unavailable`: the screening provider didn't answer. Safe to retry. `200` example (`application/json`): ```json [ { "level": 2, "name": "Standard", "description": "National criminal database, county search, SSN trace.", "price": 34.95, "feesNote": "Plus pass-through fees." } ] ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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 ### 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. `status` is `completed` once no check is `QUEUED` or `PENDING`. - **`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 in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```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`): ```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`): ```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`): ```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. Follow `nextCursor` until it is null. - **`400`**: `invalid_cursor`: `cursor` isn't a `nextCursor` we issued; start again from the first page. (`invalid_json`: `updated_since` or `limit` isn't a valid value.) - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found`: no client with that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "items": [ { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "stage": "complete", "level": 2, "expirationDate": "2028-10-18T18:22:41Z", "completedAt": "2026-10-18T18:22:41Z", "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-18T18:22:41Z" } ], "nextCursor": "NjM4OTYxNjU3NjE0MDAwMDAwOjEwNDI" } ``` `400` example `invalid_cursor` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`): ```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: ```json { "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 (`status` `CREATED`) and the applicant has been emailed. Billable. `Location` points at the check. - **`400`**: `validation_failed`: a field is missing or invalid (for example no `level`). 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 that `clientId` or `externalClientId` exists under your account. Don't retry. - **`409`**: Nothing was ordered or billed. Two cases, told apart by `code`: `duplicate_check` (this body: the applicant already has a check in progress or unexpired, in `order`), or `external_identifier_in_use` (problem details: the `externalIdentifier` belongs 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 in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: Two cases. If the body is a check (`status` `PENDING`, no `code`), the submission's answer was lost: the check may exist upstream, is kept `PENDING`, and is resolved automatically within about an hour (`check.updated` follows). Retry only with the same `externalIdentifier`, never with `forceReorder`. If the body is problem details with `upstream_unavailable`, nothing was created or recorded, and a retry is safe. `201` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CREATED", "stage": "awaiting_applicant", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T15:10:01Z" } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`): ```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`): ```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`): ```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`): ```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: ```json { "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 this `batchReference` already 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's `result`. Billable per submitted check. - **`400`**: `validation_failed`: a problem with the batch itself: no applicants, more than 500, a missing or invalid `level`, or a field too long. Problems with single applicants never fail the batch; they come back as `invalid` 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. - **`404`**: `client_not_found`: no client with that `clientId` or `externalClientId` exists 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 once `GET /batches/{batchId}` shows the other one `completed`. - **`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 in `Retry-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`): ```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`): ```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`): ```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`): ```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`): ```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`): ```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`): ```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`): ```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`): ```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: ```json { "emails": [ "jordan.kim@example.com", "sam.ortiz@example.com" ] } ``` Responses: - **`200`**: One item per email, in request order, keyed by `index`. `check` is the latest check for that email at this client, or null. Emails are never returned. - **`400`**: `validation_failed`: `emails` is 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 that `clientId` or `externalClientId` exists under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "items": [ { "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`): ```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`): ```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`): ```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`): ```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_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CREATED", "stage": "awaiting_applicant", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T15:10:01Z" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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: ```json { "externalIdentifier": "vol-8841" } ``` Responses: - **`200`**: The check, now with your `externalIdentifier`. - **`400`**: `validation_failed`: `externalIdentifier` is missing or longer than 100 characters. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`409`**: `external_identifier_in_use`: the check already has an `externalIdentifier` (it can be set once), or another of your checks uses that value. Nothing changed. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CREATED", "stage": "awaiting_applicant", "level": 2, "expirationDate": null, "completedAt": null, "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T15:10:01Z" } ``` `400` example `validation_failed` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`): ```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`): ```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 now `CANCELLED`. If `upstreamCancelled` is false the screening provider didn't confirm it and the check may still be processed and billed: contact support. - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CANCELLED", "stage": "closed", "level": 2, "expirationDate": null, "completedAt": "2026-10-16T16:00:00Z", "createdAt": "2026-10-16T15:10:00Z", "updatedAt": "2026-10-16T16:00:00Z", "upstreamCancelled": true } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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`**: OK - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: Not Found - **`409`**: Conflict - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. - **`502`**: Bad Gateway `401` example `invalid_api_key` (`application/problem+json`): ```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`): ```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`): ```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`): ```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`): ```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 new `check.updated` delivery with the check's current state was queued (a new `eventId`). - **`401`**: `invalid_api_key`: the API key is missing, invalid or revoked, or it's a test key on production (or the reverse). Don't retry until the key is fixed. - **`404`**: `client_not_found` or `check_not_found`: the client, or the check within it, doesn't exist under your account. Don't retry. - **`429`**: `rate_limited`: too many requests with an invalid API key from your IP address. Wait the number of seconds in `Retry-After`. Requests with a valid key are never throttled this way. `200` example (`application/json`): ```json { "message": "Webhook replay request accepted", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "status": "QUEUED" } ``` `401` example `invalid_api_key` (`application/problem+json`): ```json { "type": "https://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`): ```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`): ```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. ```js // Node.js 20+ (rawBody is a Buffer of the exact request body) import crypto from "node:crypto"; const expected = crypto.createHmac("sha256", process.env.DATASOURCE_WEBHOOK_SECRET).update(rawBody).digest("hex"); const given = Buffer.from(req.get("X-Partner-Signature") ?? "", "utf8"); const valid = given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, "utf8")); ``` ```python # Python 3 import hashlib, hmac, os expected = hmac.new(os.environ["DATASOURCE_WEBHOOK_SECRET"].encode("utf-8"), raw_body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, request.headers.get("X-Partner-Signature", "")) ``` ```csharp // C# var expected = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), rawBody)).ToLowerInvariant(); var valid = CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signatureHeader ?? "")); ``` **Delivery.** Respond with any `2xx` within 15 seconds; do slow work asynchronously. Anything else is retried up to 5 more times with increasing delays. Delivery is at least once and not ordered: drop repeats by `eventId` (the same on every retry of one event), and keep the state with the latest `timestamp` for each check (key checks by `id`). | Field | Type | Required | Description | |---|---|---|---| | `event` | `string (one of "check.updated")` | no | Event type. Currently always `check.updated`. | | `eventId` | `string (uuid)` | no | Unique id of this event, the same on every retry of its delivery. Use it to drop duplicate deliveries. | | `id` | `string` | no | Identifier of the check (`chk_…`, same as `id` in API responses). | | `externalIdentifier` | `string | null` | no | Your identifier for the check, as sent when ordering. The easiest way to match the webhook to your records. | | `clientId` | `string` | no | Client the check belongs to (its `clientId`). | | `status` | `string (one of "QUEUED", "PENDING", "CREATED", "CLEAR", "REVIEW", "ERROR", "CANCELLED")` | no | `QUEUED`: in a batch, not yet submitted. `PENDING`: accepted, being submitted. `CREATED`: submitted; the applicant has been invited to complete their information. `CLEAR`: completed, no search found records. `REVIEW`: completed and flagged (records found, or the result needs a person to look). `ERROR`: could not be completed. `CANCELLED`: cancelled. `CLEAR`, `REVIEW`, `ERROR` and `CANCELLED` are final. | | `stage` | `string (one of "queued", "submitting", "awaiting_applicant", "applicant_ready", "in_progress", "complete", "closed")` | no | Where the check is, finer than `status`: `queued` (in a batch, not yet sent), `submitting` (being sent), `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: ```json { "event": "check.updated", "eventId": "e7a1c3b5-2d4f-4a6b-8c9d-0e1f2a3b4c5d", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "stage": "complete", "level": 2, "expirationDate": "2028-10-18T18:22:41Z", "completedAt": "2026-10-18T18:22:41Z", "timestamp": "2026-10-18T18:22:42Z" } ``` ## Webhook `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: ```json { "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: ```json { "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: ```json { "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: ```json { "event": "check.expiring", "eventId": "a4c6e8f0-2b4d-4f6a-8c0e-1a3c5e7f9b2d", "timestamp": "2028-09-18T14:00:03Z", "id": "chk_9c2e4f6a1b3d4e5f8a7b6c5d", "externalIdentifier": "vol-8841", "clientId": "cl_5b0e6f2a8c1d4e7b9a3f2d6c", "status": "CLEAR", "expirationDate": "2028-10-18T18:22:41Z", "daysLeft": 30 } ``` ## Webhook `ping`: Test ping Sent by `POST /settings/webhook-test` so you can verify your endpoint and your signature check. Same signature, retry and `eventId` rules as `check.updated`. | Field | Type | Required | Description | |---|---|---|---| | `event` | `string (one of "ping")` | no | Always `ping`. | | `eventId` | `string (uuid)` | no | Unique id of this event, the same on every retry of its delivery. Use it to drop duplicate deliveries. | | `timestamp` | `string (date-time)` | no | When this webhook was generated (UTC). | Example: ```json { "event": "ping", "eventId": "f1d3b5a7-9c2e-4d6f-8a0b-3c5e7f9a1b2d", "timestamp": "2026-10-16T15:00:00Z" } ``` --- # Changelog Changes to the partner API, newest first. Changes are additive; a breaking change gets 90 days' notice here and by email. ## 2026-11-14 - Fixed (docs): the OpenAPI `stage` enum now lists `applicant_ready`, which checks have reported since 2026-10-27. The `duplicate_check` example in the error catalog now shows the current check shape (`id` is a `chk_...` string), and the guide says that a webhook retry resends the identical body (same `eventId` and `timestamp`). No API behaviour changed. - New: sandbox keys are self-serve. Sign up at https://join.cbgc.io/sandbox/, confirm your email, and get a `pk_test_` key and webhook secret (shown once). Production keys still come from Datasource once your sandbox integration works. ## 2026-10-27 - New: `check.expiring` webhook, 30 days and again 7 days before a `CLEAR` or `REVIEW` check's `expirationDate`. - New: `POST /clients/{clientRef}/checks/{checkRef}/remind` re-sends the applicant's invitation (at most hourly, while the check is `awaiting_applicant`). New errors `check_not_awaiting_applicant` and `reminder_too_soon`. - New: `reportUrl` on `CLEAR` and `REVIEW` checks (the report in the church's portal; needs the church's sign-in) and `charges` (US dollars) on finished checks, on the check and in `check.updated`. - New: an invitation that expires before the applicant finishes closes the check as `ERROR` with `errorCode` `invitation_expired`. - New: `GET /settings/webhook-deliveries`, the last 30 days of webhook delivery attempts and how your endpoint answered. - New: `errorCode` and `errorMessage` on `ERROR` checks and in their `check.updated`: why the check failed and what to fix (for example `missing_job_location`). Null for every other status. - Changed: `completedAt` (and so `expirationDate`) is the screening provider's own completion time when it gives one, not the time we learned of it. - New: `GET` and `PUT /clients/{clientRef}/settings` with `checkValidityYears` (1 to 5, or null): how long that church's `CLEAR` and `REVIEW` checks stay valid. It overrides your account's default (`checkValidityYears` in `PUT /settings`), which still applies to churches without their own. - New: `checkValidityYears` in `PUT /settings` (1 to 5, default 2): how long a `CLEAR` or `REVIEW` check stays valid. `expirationDate` is now `completedAt` plus that many years, and is set only on `CLEAR` and `REVIEW`. Before, an open check could carry the expiry of the applicant's invitation link as its `expirationDate`; it no longer does. - New: `stage` `applicant_ready`. The applicant finished the form, but the order is held for the church to review and release in its Datasource portal (usually a possible duplicate or a jurisdiction question). Show it to the church's staff as needing action, with the client's `portalUrl`. Once released, the stage becomes `in_progress`. The status stays `CREATED`. - Fixed: a `forceReorder` for an applicant whose check is still open, or with a different `externalIdentifier`, is now a new check with its own `id`. Before, it replaced the open check (its `externalIdentifier` changed and its order was no longer tracked). Ordering again with the same `externalIdentifier` still reuses that check, and now clears its old `completedAt` and `expirationDate`. - New: `stage` `in_progress` is now reported. A `CREATED` check moves from `awaiting_applicant` to `in_progress` when the applicant finishes the form, and you get `check.updated`. The status stays `CREATED`. - Changed: `check.updated` is sent only when a check's status or stage changes, not on every update from the screening provider. - New: optional `applicantPhone` on orders (`phone` per batch applicant): the applicant's mobile number. When sent, the invitation is also texted to it; the email is still sent. ## 2026-10-21 Breaking, made before go-live (no partner is live yet): - Changed: clients are identified by `clientId` (`cl_` followed by 24 characters), which replaces `clientGuid` in every response and webhook (`check.updated`, `batch.completed`, `client.connected`, `client.migrated`) and in `GET /connect/sessions/{sessionId}`. `{clientRef}` accepts `clientId` or your `externalClientId`. - Changed: a check's `id` is a string (`chk_` followed by 24 characters) set when the check is ordered. `orderId` is removed from checks and `check.updated`; the replay-webhook response has `id` instead of `orderId`. The check path segment is `{checkRef}`: the check's `id` or your `externalIdentifier`. - Changed: a completed check is `CLEAR` when the screening provider reports that no search found records, and `REVIEW` (flagged) otherwise. Before, every completed check was `REVIEW`. A provider status of "pending review" is now `CREATED` (in progress), not `REVIEW`. - New: optional `jobLocation` and `stateOfEmployment` on orders and batches (and per batch applicant): where the applicant will work, as the church gives it. Some levels require both; we pass them on unchanged and never fill them in. - Removed: `GET /clients/{clientRef}/packages` and ordering by `packageCode`. Order by `level` (`GET /products`). - Changed: a `cursor` from before this change is `400 invalid_cursor`; start the listing again without one. - Changed: a contact phone number must be a 10-digit US or Canadian number (any format, e.g. `801-555-1234`); anything else is `400 validation_failed` instead of `502`. ## 2026-10-17 - New: `eventId` on every webhook (the same on every retry of one event). Use it to drop repeated deliveries. - New: `status` on `client.connected` (always `Active`). - Changed: an `externalIdentifier` is unique across all your clients. Ordering with one that belongs to another client's check is `409 external_identifier_in_use` (an `invalid` item in a batch); before, it re-used that check. - Changed: a check left `PENDING` by a lost submission answer now resolves on its own within about an hour (its real status, or `ERROR` if it never reached the screening provider), with `check.updated`. - Changed: a completed result is `CLEAR` only when the screening provider says it is clear; otherwise `REVIEW`. - Changed: `502 upstream_unavailable` on ordering when the church's account couldn't be prepared: nothing was created, and a retry is safe. - Changed: `invalid_cursor` is now the `code` for a bad `cursor` (it was `validation_failed`). - New: problem responses written by the framework carry a `code` too: `invalid_json`, `not_found`, `method_not_allowed`, `payload_too_large`, `unsupported_media_type`, `internal_error`, `request_failed`. - `POST /clients` no longer returns `temporaryPassword`: like the connect page, the primary contact gets their sign-in by email from Datasource. The response has `welcomeEmailSent` instead, and `primaryUsername` may be a derived name when the email is already a login. - Connect page: the church's primary contact gets a sign-in email from Datasource with a temporary password. Submitting the page twice is safe. Invoice billing takes the billing setup copied from your account. - Docs: the guide is reorganized into 16 chapters with field tables, limits, status transitions, a reconciliation recipe and a test plan; the reference has enums, formats, limits and examples for every type; `/llms-full.txt` includes every operation's parameters and full JSON examples. ## 2026-10-16 - Errors now carry a stable `code` and a `type` URI that points at `/docs/errors.md`. - Hosted documentation: `/docs/guide.md`, `/docs/errors.md`, `/docs/changelog.md`, `/llms.txt`, `/llms-full.txt`. - New: `externalClientId` on clients; `{clientRef}` accepts it in every client route; `GET /clients?externalClientId=`. - New: product levels (`GET /products`, `GET /clients/{clientRef}/products`) and `level` on orders. - New: `orderedBy` on orders (recorded as an order note only). - New: `POST /clients/{clientRef}/checks/batch` and `GET /batches/{batchId}`. - New: `stage` on checks and `check.updated`; `QUEUED` status; `portalUrl` on clients. - New webhook events: `batch.completed`, `client.connected`, `client.migrated`, `ping`. Ignore events you don't know. - New: partner settings (`/settings`): register your webhook and return URLs, rotate the webhook secret, send a test `ping`. - New: the hosted connect page (`POST /connect/sessions`, `GET /connect/sessions/{sessionId}`). - New: `POST /clients/{clientRef}/checks/lookup` (latest check per email, by index; emails are never returned) and `PATCH /clients/{clientRef}/checks/{orderId}` (set your own `externalIdentifier` once), for linking a migrated church's history to your people.