# 1 · enrol: a reference signature for your customer cust-88
curl -X PUT https://api.unforged.sh/nib/subjects/cust-88/references \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-09-27" \
-H "Idempotency-Key: enrol-cust-88-1" \
-F [email protected]
2 · check
# 2 · check: a document, with your own references
curl -X POST https://api.unforged.sh/nib/checks \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-09-27" \
-H "Idempotency-Key: app-1042" \
-F 'request={"process":"loan-application","items":[{"file":"q","subject_reference":"cust-88","reference":"APP-1042"}]};type=application/json' \
-F [email protected]
3 · read
# 3 · read: a queued job's status and results
curl https://api.unforged.sh/nib/checks/app-1042 \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-09-27"
find by your reference
# find a check again by your reference
curl "https://api.unforged.sh/nib/checks?view=items&reference=APP-1042" \
-H "Authorization: Bearer $UNFORGED_KEY" \
-H "Unforged-Version: 2026-09-27"
Drop files in nib/inbox/<job>/, write manifest.json last.
The answer lands in nib/results/<job>/.
bucket drop
# any S3 tool, with your bucket's keys: the files first, the manifest last
aws s3 cp ./batch/ s3://$UNFORGED_BUCKET/nib/inbox/2026-10-batch-07/ --recursive
aws s3 cp manifest.json s3://$UNFORGED_BUCKET/nib/inbox/2026-10-batch-07/manifest.json
# nib/inbox/2026-10-batch-07/manifest.json
{
"items": [
{
"file": "app-1041.pdf",
"subject_reference": "cust-88",
"template_id": "benefits-form-v3",
"reference": "APP-1041"
},
{
"file": "app-1042.jpg",
"references": [
"refs/extra-1042.png"
],
"target": {
"page": 1,
"box": "applicant_signature"
},
"reference": "APP-1042",
"metadata": {
"branch": "leeds"
}
}
],
"policy": "default",
"process": "loan-application"
}
# the answer: nib/results/2026-10-batch-07/summary.json
# (one result per line in nib/results/2026-10-batch-07/results.ndjson)
{
"job_id": "inv-2291",
"status": "completed",
"total": 2,
"completed": 1,
"failed": 1,
"counts": {
"APPROVE": 1
},
"errors": [
{
"item_index": 1,
"file": "missing.png",
"field": null,
"type": "file_not_found",
"detail": "missing.png is not in the job folder"
}
],
"metadata": {
"batch": "2026-10"
},
"versions": {
"model": "nib-2.2",
"policy": "default@7",
"api_version": "2026-09-27"
},
"started_at": "2026-09-25T10:15:30.123Z",
"completed_at": "2026-09-25T10:15:32.456Z"
}
One result per item: the same JSON inline, from GET, streamed, and in your
bucket. See CheckResult.
The API version (YYYY-MM-DD) to use for this request, overriding the organisation's pinned version (it never changes the pin). An unknown or malformed version, or one past its sunset, is 400 invalid_version.
Unforged-Org
Act in a descendant organisation: its WorkOS organisation id (^[A-Za-z0-9_]{1,64}$, else 400 invalid_request). Needs org:children:access, and the organisation must be an active descendant of the caller's (and inside the caller's subtree when the caller's organisation requires SSO); otherwise 403 permission_denied, the same answer whether or not the organisation exists. The request then runs in the descendant with the caller's permissions minus org:keys:manage, org:billing:manage and org:children:manage, in the caller's API version. Every such request is audited in the descendant (org.act_as, with the caller's organisation as actor_org_id). Your own organisation's id is a no-op.
Idempotency-Key
On POSTs: the same key and body replay the first answer for 24 hours.
API reference
Nib API
Checks
Check documents (nib:checks:create) and read results (nib:checks:read).
Search and list checks (jobs, or checked items), newest first
nib:checks:readRead checks and results
view=jobs (the default) lists jobs; view=items lists checked items with your references, for search: exact filters on reference, process, subject_reference, decision and input_type, and q, a prefix of an item's reference, its subject_reference, its file name or its check id (job_id, or job_id/index). metadata is never searched, and lists leave it out (read the item for it). Both views take since and until (on created_at) and page with an opaque cursor: pass the previous page's next_cursor (null on the last page). Cursors are stable: checks made while you page never shift a page. A filter of the other view (status and source are jobs-only) is 400 naming it; so is a cursor this route did not issue.
Parameters of GET /nib/checks
Parameter
In
Type
view
jobs, items, or summary: the checked items the item filters select, counted per decision (ERROR: items without one), with the processes seen in the date range (no decision, cursor or limit).
view=items and view=summary: your check reference, exactly.
query
CheckReference
process
view=items and view=summary: the process, exactly.
query
Process
subject_reference
view=items and view=summary: the subject reference, exactly.
query
Id
decision
A comma list of decisions, plus ERROR for items without one (an item error). On GET /nib/checks, view=items only.
query
string
input_type
view=items and view=summary: e.g. photo, scan, form, signature_image.
query
string
q
view=items and view=summary: a prefix of the reference, the subject reference, the file name or the check id (job_id or job_id/index); case-sensitive, at most 128 characters.
query
string
Responses of GET /nib/checks
Status
Body
Meaning
200
One page of jobs (view=jobs) or of checked items (view=items), or the counts (view=summary)
A multipart body with one JSON part request (a CheckRequest) plus one file part per uploaded file, named as the items name them (file, references); or a JSON CheckRequest whose inputs are all URLs ({"url": "https://…"}, fetched once each from your allowlisted hosts and S3 buckets). All the URL fetches of one request share a 60 s deadline (then 504 timeout with reason: fetch_deadline); a body arriving slower than 64 KiB per 10 s fails with fetch_failed, reason: body_stalled.
The job id is request.job_id, else the Idempotency-Key. Exactly one item whose document has at most 10 pages is checked in the request: 200 with the check result. Anything larger is queued: 202 with job_id and status_url (also in Location). If no capacity frees up within the budget the job continues asynchronously and the answer is 503 busy with job_id and status_url; if the item exceeds its 30 s budget, 504 timeout with the same members.
Streaming. With Accept: text/event-stream (exactly one item) the answer is a Server-Sent Events stream (see x-sse-events on the 200 response). The stream closes after decision or error; a : keep-alive comment is sent every 10 s. A stream is not replayable: retrying its Idempotency-Key is 409 idempotency_not_replayable pointing at GET /nib/checks/{job_id}.
Session tokens may call this route only streamed, with one item whose subject_reference is the session's subject and no references, URL inputs, policy, target, template_id (403) or job_id (400). They send no Idempotency-Key.
Parameters of POST /nib/checks
Parameter
In
Type
Idempotency-Keyrequired
1–255 visible ASCII characters, kept 24 h per tenant. On POST /nib/checks it is also the job id when the request names none.
header
string
Accept
text/event-stream streams the check (Server-Sent Events).
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
Results are paginated by item_index: pass next_cursor as cursor. With format=ndjson a finished job's result lines stream as application/x-ndjson (409 conflict while it runs). decision keeps only the items with those decisions (ERROR: items without one); the pagination is unchanged.
Parameters of GET /nib/checks/{job_id}
Parameter
In
Type
job_idrequired
path
Id
cursor
The previous page's next_cursor (the last item_index returned).
query
string
limit
query
integer
format
query
"json" | "ndjson"
decision
A comma list of decisions, plus ERROR for items without one (an item error). On GET /nib/checks, view=items only.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
A grey PNG of one located signature of the item: its box with 8% padding on each side, at most 640 px on the long side, rendered from the item's input. The crop is kept beside that input (nib/inbox/<job_id>/.crops/) and never outlives it: it expires with the inbox (inbox_ttl, 24 h by default) and with the job's history. 404 when the item has no located signature, or when its input has expired (detail: "the input has expired"). Images have their own rate limit (50/s, burst 200): they never use your API rate.
Parameters of GET /nib/checks/{job_id}/items/{index}/image
Parameter
In
Type
job_idrequired
path
Id
indexrequired
The item's index in the request (decimal, no sign or leading zero).
path
integer
signature
The signature's index (default the best match, else the first).
query
integer
Responses of GET /nib/checks/{job_id}/items/{index}/image
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
Records a reviewer's confirmation (the first is kept; audited as check.confirm) and, unless your tenant's learning is off, queues the item for learning. The entry bar is applied later (parent §8.2). No body. See docs/api/learning.md.
Parameters of POST /nib/checks/{job_id}/items/{index}/confirm
Parameter
In
Type
job_idrequired
path
Id
indexrequired
The item's index in the request (decimal, no sign or leading zero).
path
integer
Idempotency-Keyrequired
1–255 visible ASCII characters, kept 24 h per tenant. On POST /nib/checks it is also the job id when the request names none.
header
string
Responses of POST /nib/checks/{job_id}/items/{index}/confirm
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
nib:subjects:readRead subjects/references metadata and thumbnails
Your subjects with their reference counts (live references; anchors are the ones you enrolled, learned the ones Nib learned) and their last check. q is a prefix of the subject reference. Page with cursor (the previous page's next_cursor).
nib:subjects:readRead subjects/references metadata and thumbnails
Also the subject's checks over time: checks.recent holds its newest 25 checked items (as in GET /nib/checks?view=items), checks.total counts them all, and checks.next lists them all.
Parameters of GET /nib/subjects/{subject_reference}
Parameter
In
Type
subject_referencerequired
Your id for the person.
path
Id
Responses of GET /nib/subjects/{subject_reference}
Adds to the subject's references, creating the subject if new. Multipart: one file part per reference, named by its ref_id (PNG, JPEG, TIFF or PDF; at most 20 files and 20 MB in total), each optionally with a text field source.<ref_id>. Or JSON: references by URL. An upload replaces that ref_id. Up to 3 references are enrolled in the request (200); more are queued (202, each queued). ref_ids starting with learned- are reserved (400). A PUT needs no Idempotency-Key: it is idempotent.
Parameters of PUT /nib/subjects/{subject_reference}/references
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
nib:subjects:readRead subjects/references metadata and thumbnails
A grey PNG of the stored reference (its first page), at most 512 px on the long side. 404 when the reference or its file is gone, or it was unlearned. Images have their own rate limit (50/s, burst 200): they never use your API rate.
Parameters of GET /nib/subjects/{subject_reference}/references/{ref_id}/image
Parameter
In
Type
subject_referencerequired
Your id for the person.
path
Id
ref_idrequired
path
Id
Responses of GET /nib/subjects/{subject_reference}/references/{ref_id}/image
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
nib:references:deleteDelete references and subjects, unlearn
Removes learned references by time (since, by check time) or by source check, deletes their files and blocks those checks from ever being learned again. since blocks the checks recorded up to this call; a job still running then (submitted inside the window) records its checks later and is not blocked. Audited (reference.unlearn). See docs/api/learning.md.
Parameters of POST /nib/subjects/{subject_reference}/references/unlearn
Parameter
In
Type
subject_referencerequired
Your id for the person.
path
Id
Idempotency-Keyrequired
1–255 visible ASCII characters, kept 24 h per tenant. On POST /nib/checks it is also the job id when the request names none.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
For end-user apps, which never hold an API key. The token is limited to one subject and one streamed check, works only on GET /nib/live and streamed POST /nib/checks, and is shown once (Cache-Control: no-store). No Idempotency-Key (a stored response would keep the token): a retry mints another session. 404 for an unknown subject. While the organisation (or one above it) is suspended, its session tokens are refused with 401 unauthorized, like expired ones.
A WebSocket upgrade. Authenticate with an API key or dashboard token holding nib:checks:create, or a session token in Authorization: Bearer …; browsers, which cannot set that header, send Sec-WebSocket-Protocol: unforged.session, <session token> and the server selects unforged.session.
Send each downscaled preview frame as one binary message: a PNG or JPEG of at most 640 px on its long side and 512 KB, at most 5 per second. Each message gets exactly one JSON text reply, in order (see x-websocket-messages). Frames are analysed in memory and never stored.
A connection closes after 60 s without a data message, after 10 minutes, and when its credential is no longer valid (close code 1008, checked every 30 s): the session expires or is revoked, the API key is revoked, or the organisation (or the one acted in with Unforged-Org) is suspended. At most 20 open connections per tenant (429 before the upgrade) and 200 per instance (503).
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
201 with the new version; 200 with the latest one when the body equals it (a retried PUT does not mint versions). Versions are immutable; checks use the latest version of the policy they name and record name@version in versions.policy. Audited.
Multipart: a template JSON part (TemplateCreate) and a blank file part (PNG or PDF, at most 20 MB). 409 when the id exists (delete it first) or you have 200 templates. Audited.
Parameters of POST /nib/templates
Parameter
In
Type
Idempotency-Keyrequired
1–255 visible ASCII characters, kept 24 h per tenant. On POST /nib/checks it is also the job id when the request names none.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
The secret is shown once (Cache-Control: no-store). At most 2 active keys (409). The body, if any, must be {}. No Idempotency-Key: a retry issues another key. Audited.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
The tenant's history retention (default 90 days), the results/ lifecycle backstop derived from it, and the audit log's fixed retention. See docs/api/retention.md.
org:settings:manageRetention, org-wide settings, the API version pin
Finished jobs (jobs, their result rows, nib/results/<job_id>/ and anything left in nib/inbox/<job_id>/) are purged once older than the retention, within about a minute. A job that a live learned reference came from is kept until that reference is unlearned or evicted. Usage records (billing) and the monthly quota count are never affected. A change is audited (settings.retention.set); setting the current value again is not.
Whether accepted signatures can become extra references of their subject, and the bar they must reach. The defaults (learning off, 0.99, 10) until they are set. Also terms_accepted, the tenant's most recent biometric-retention terms acceptance (null if none was ever recorded; Task 9 fix round 1). See docs/api/learning.md.
Replaces the three settings (all are required). learn_min_band_lo is 0.5 to 1 and max_learned 0 to 50; a refusal is 400 naming the field. Turning learning on (from off) is refused with 409 learning_terms_required unless the tenant has accepted the current biometric-retention terms (POST /nib/settings/learning/terms); turning it off is always allowed. The next check uses the new settings. A change is audited (settings.learning); setting the current value again is not.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
The URL must be https (port 443, or an explicit port ≥ 1024) and resolve to public addresses only. Before the endpoint is created, a webhook.test event signed with the new secret is posted to the URL: anything but a 2xx within 10 s is 400 webhook_test_failed and nothing is created. The secret (whsec_…) is shown once (Cache-Control: no-store). At most 20 endpoints (409). 503 not_ready while the service has no master key. No Idempotency-Key. See docs/api/webhooks.md.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
A new secret (whsec_…, shown once, Cache-Control: no-store) replaces the old one at once: every delivery from now on, including retries of earlier events, is signed with the new secret only. Update your endpoint's secret straight away. 503 not_ready while the service has no master key. No Idempotency-Key. Audited.
Parameters of POST /nib/webhooks/{webhook_id}/secret/roll
Parameter
In
Type
webhook_idrequired
path
string
Responses of POST /nib/webhooks/{webhook_id}/secret/roll
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
Queues a new delivery of the same event (same id and body). 503 not_ready while the service has no master key. Audited. The delivery log keeps attempts 30 days; an older delivery_id is 404.
Parameters of POST /nib/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
Parameter
In
Type
webhook_idrequired
path
string
delivery_idrequired
path
string
Idempotency-Keyrequired
1–255 visible ASCII characters, kept 24 h per tenant. On POST /nib/checks it is also the job id when the request names none.
header
string
Responses of POST /nib/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
busy (no capacity; Retry-After; a check also carries job_id and status_url and continues asynchronously), not_ready, or org_moving (the organisation is being moved to another region; Retry-After)
One UTC calendar month (period: current, the default, or YYYY-MM). checks and billable_checks count the checked items and the billable ones (a decision of AUTO_APPROVE, APPROVE, FLAG or FAILED); not_charged and decisions count the month's checked items by decision (errors: items without one). The estimate is $0.50 per billable check. For an organisation billed through its parent (billing.mode: parent) the parent is invoiced; the amount is still shown. daily lists every day of the month that has begun.
org:keys:manageCreate/revoke API keys (within own permissions)
The key is shown once (Cache-Control: no-store); only its display prefix, its id and the hash of its secret are stored. It carries the organisation's home region as a routing hint (not a secret: the secret part is). No Idempotency-Key: a retry issues another key. Audited.
org:keys:manageCreate/revoke API keys (within own permissions)
204 also when already revoked. As for creation, the key's permissions must be within the caller's own (else 403 permission_denied naming the excess; an owner holds every permission). An API-key caller cannot revoke the last active key holding org:keys:manage (409). Takes effect on the next request. Audited.
conflict, idempotency_conflict (the key was used with another body, or the job id with other inputs), idempotency_in_progress (the first request still runs) or idempotency_not_replayable (the key's response was a stream)
Exists only when the operator configured a metrics token (UNF_METRICS_TOKEN); else 404 exactly like an unknown route. The token (not an API key) is required as a bearer token (401 otherwise).
tenant_not_provisioned (403): a dashboard token whose organisation has no tenant. billing_required (402): the tenant has no active billing. invalid_signature (400): a webhook whose signature does not verify. billing_already_active (409): the tenant already has a subscription; manage it in the billing portal. invalid_version (400): an Unforged-Version header that is malformed, unknown or past its sunset. org_suspended (403): the caller's organisation, or an organisation above it, is suspended (API keys and dashboard users alike). wrong_region (421): the caller's organisation lives in another region (see region and api_host). org_moving (503): Unforged is moving the organisation to another region; every request for it answers this, with Retry-After, until the move ends. learning_terms_required (409): PUT /nib/settings/learning refuses to turn learning on until the tenant accepts the current biometric-retention terms (see terms_version; POST /nib/settings/learning/terms). webhook_test_failed (400): POST /nib/webhooks sent the new endpoint a signed webhook.test event and got no 2xx within 10 s; nothing was created. The detail says why (the HTTP status, a timeout, or a failed connection).
request_id
string
region
string
On wrong_region, the caller's home region.
api_host
string
On wrong_region, that region's API host.
terms_version
string
On learning_terms_required, the terms version the tenant must accept.
job_id
string
On busy / timeout of a check that continues asynchronously.
status_url
string
host
string
On a failed URL fetch (fetch_failed, not_allowlisted, too_large, timeout).
reason
string
On a failed URL fetch: a reason code such as http_status, connect_failed, body_stalled (the body arrived slower than 64 KiB per 10 s), timeout or fetch_deadline (a 504: all the URL fetches of one request share a 60 s deadline).
In a fixed order that never depends on enrolment order: the subject's anchors by ref_id, then the item's references by manifest path, then learned references by ref_id. input_hashes.references follows the same order.
flags*
string[]
Integrity and forensic flag codes, then soft flags. Soft flags cap the decision at FLAG and never fail a signature on their own: different_instrument (reason "Pen, pressure or writing speed differs from the references"), unsteady_strokes (reason "Stroke smoothness differs from the references (tremor or hesitation)") and slow_uniform_strokes (reason "Strokes are slower and more uniform than the references (possible tracing)").
What produced a result: the Unforged Nib model version, your policy and the API version of the document's shape.
Fields of Versions
Field
Type
Description
model*
string
The model version, e.g. nib-2.2 (Unforged Nib v2.2).
policy*
string | null
name@version; null when no item reached the policy.
api_version*
string
Metadata
Your own key/value pairs, echoed back and never indexed: at most 20 pairs, keys 1–40 characters, string values of at most 500 characters.
CheckReference
Your own id for a check, e.g. an application number: 1–128 printable characters (no control characters). Not required to be unique.
Process
Your business process, e.g. loan-application: 1–64 characters of [a-z0-9._-]. Used for filtering and reporting.
ItemError
Fields of ItemError
Field
Type
Description
type*
string
detail*
string
CheckResult
One item's result, identical in API responses (sync, GET, NDJSON, SSE) and nib/results/<job_id>/results.ndjson. Every field is always present (null when absent). Your references (subject_reference, reference, process, metadata) are echoed from the item. anchor_probability is the best match against the subject's enrolled references alone whenever they were loaded; null when the subject has none.
Fields of CheckResult
Field
Type
Description
id*
string
ver_<ulid>
item_index*
integer
file*
string | null
subject_reference*
string | null
Your id for the person whose enrolled references were compared.
Document notes: stable codes, code or code:detail, pages numbered from 1. Among them: colour_budget_exceeded:<page>: stamp removal was skipped on that page, because its colour plane did not fit the document's colour budget (the page was read in grey, as a grey scan would be).
nib/results/<job_id>/summary.json: counts per decision and item errors. A rejected bucket drop has one job error (item_index: null) of type manifest_changed or quota_exceeded. A manifest whose reference, process or metadata breaks its limits fails with one error of type invalid_request naming the item (item_index, null for a top-level default) and the field, like the API's 400.
The bucket-drop manifest, nib/inbox/<job_id>/manifest.json, written last. At most 10 000 items; invalid items fail individually, except that a reference, process or metadata out of its limits rejects the whole manifest (a failed summary.json naming the item and the field). No path segment may be .api-submission (reserved). Objects outside nib/ are not read.
Monthly quota. A bucket drop counts its item count against the monthly check-item quota exactly like POST /nib/checks. A new job that would exceed it is not registered (no job, nothing checked or billed): nib/results/<job_id>/summary.json is written create-only with status: rejected and one job error of type: quota_exceeded. Writing the manifest again (a new object version) retries once the quota allows it; the checked job's own summary then replaces the rejection.
An event you subscribed to, or webhook.test (the connection test sent once when the endpoint is created).
created_at*
string
data*
object
The event's data (see docs/api/webhooks.md). job.completed carries job_id, status, total, counts, failed, versions, status_url, items (the first 100 results, each exactly as CheckResult) and items_truncated; a job of more than 100 items also has items_url, the paged GET of the rest. The body is rendered in your pinned API version, sent as Unforged-Version.
A contract's state today (a sub-organisation's is its parent's); null otherwise.
billed_through
string | null
For an organisation billed through its parent (mode: parent), the name of the organisation that pays; null otherwise. Only in GET /me.
promo_credit
object | null
The sign-up credit (see GET /billing/plan; null without one), as of the organisation's cached row (a few seconds old at most; GET /billing/plan reads it afresh). Only in GET /me.
mode*
"stripe" | "invoiced" | "parent"
stripe: metered self-serve billing; invoiced: billed outside Stripe; parent (a child organisation): billed through its parent organisation.
status*
"pending" | "active" | "past_due" | "canceled"
pending until the first Checkout completes (checks run on the sign-up credit while any is left); past_due while Stripe retries a payment (checks still run); canceled after the subscription ends. Invoiced tenants are always active.
BillingUrl
Fields of BillingUrl
Field
Type
Description
url*
string
The Stripe page to send the user to.
TenantPlan
Fields of TenantPlan
Field
Type
Description
quote_id
string
The accepted quote this plan comes from (history source quote).
An override of the SSO ringfence flag (normally synced from WorkOS).
BillingPlanVersion
A plan version. Contracts need ends_on (after starts_on) and contract_value_usd (staff reporting only); payg_discounted needs unit_price_usd and its customer-specific stripe_price_id; contract_commit needs committed_amount_usd, overage_unit_price_usd and exactly one of included_checks or unit_price_usd; contract_unlimited needs meter_mode. Fields that do not belong to the type are refused. Prices and amounts are positive decimal strings.
{checks} for an allowance, {usd} for a money credit.
fair_use_monthly_checks*
integer | null
this_month*
object
promo_credit*
object | null
The $10.00 sign-up credit every new organisation gets, with no card needed, read afresh (null without one: a sub-organisation, a contract). Each check with a verdict draws $0.50 from it (unreadable and unsigned documents are free). While it is not converted and anything is left, checks run without a card; a batch costing more than is left is refused (402 billing_required, naming the cost and the balance). Once a card is added (converted), nothing more is drawn: the balance left then is applied to the first invoices when the plan is plain pay as you go (payg); on any other plan it is forfeited (remaining_usd0.00).
BillingCredits
Fields of BillingCredits
Field
Type
Description
items*
object[]
One item per credit with anything left (money as US-dollar decimal strings).
The key's permissions: within the caller's own (else 403 permission_denied); an unknown name is 400 invalid_request. The presets are checks and nib-admin (docs/api/permissions.md).
ApiKey
Fields of ApiKey
Field
Type
Description
id*
string
display_prefix*
string
unf_<env>_ and the key's first 8 characters, to recognise it (keys of one region share their first few characters); never used to look a key up.