API reference
Error codes
Every code the ArbiterQA API returns, with its HTTP status and meaning. Codes are stable — handle them by code, not by message text.
| Code | Status | Meaning |
|---|---|---|
CAPTURE_PROHIBITED_HERE | 400 | A (viewport, surface) pair in capture is prohibited by the validation's own Capture Selection. details.validationId names the validation, details.prohibited lists the refused pairs, and details.reason carries the Capture Selection's recorded rationale. What a validation can be captured in ships with the module and moves only by migration, so this is not a plan limit and no upgrade changes it. GET /api/validations publishes the offered viewports and surfaces per validation. Refused before pricing, so nothing is charged and no job is created. |
CLIENT_SELECTION_NOT_IN_JOB | 400 | clients was supplied for a validation id that no email line in this job selects. details.validationId names it. Select the validation on the job, or remove its clients entry. Refused before pricing, so nothing is charged and no job is created. |
CLIENT_SELECTION_RENDERLESS_CHECK | 400 | clients was supplied for a validation that renders nothing: every functional check, and email-images-off-legibility, read the MIME, which is identical across mail clients, so they accept no client selection. details.validationId names it. Remove the clients entry. Refused before pricing, so nothing is charged and no job is created. |
CONFLICTING_PARAMETERS | 400 | Two validations[] entries set the same parameter name to values that differ. A job carries one flat parameter map, so per-validation values for the same name cannot be expressed. Align the values or drop one. details.path points at the second entry. |
DUPLICATE_VALIDATION | 400 | The same validation id appears more than once in validations[] with configurations that are not identical. Identical duplicates are collapsed automatically; differing ones are refused so neither is silently dropped. details.path names the repeat entry. |
EMPTY_CLIENT_SELECTION | 400 | clients was supplied as an empty object. "Render nothing" is expressed by not selecting the validation, not by an empty map. details.validationId names the entry. Add viewport keys with client ids (or an empty array for the native default), or remove the entry. Refused before pricing, so nothing is charged and no job is created. |
INVALID_CLIENT_SELECTION | 400 | clients on a validation entry must be an object keyed by a known viewport, each holding an array of client id strings. details.path names the offending key. Client id validity and plan gating are enforced separately. |
INVALID_ENUM_VALUE | 400 | Use one of the allowed values listed in details.allowedValues. |
INVALID_PARAMETER_VALUE | 400 | Check the submitted value against the Parameter format requirements returned in details. |
INVALID_REQUEST_BODY | 400 | Check the request schema, required fields, content type, and JSON formatting. |
JOB_ACCEPTS_ONE_SUBJECT | 400 | POST /api/jobs and POST /api/jobs/estimate examine one subject per job (one webpage or one email); validations may list any number of checks against it. details.subjectCount reports how many subjects were sent. Send one request per subject and each returns its own jobId. Refused before pricing, so nothing is charged and no job is created. |
JOB_SUBJECT_REQUIRED | 400 | Supply url on a url job; it must be an absolute http(s) URL with a host. html, when sent, must not be empty. details names the line, the field, and the problem (missing | not_http_url). Refused before pricing, so nothing is charged and no job is created. |
LEGACY_REQUEST_SHAPE | 400 | The legacy validations line-array body is no longer accepted. Send the unified job request: top-level type ("url" | "email") with its subject fields (url with optional html, or subject), validations[] as string ids or { id, severity, parameters, clients } objects, and optional validationSetId. details.hint maps each legacy field this body used to its unified home. See the POST /api/jobs reference. |
MISSING_REQUIRED_FIELD | 400 | Add the missing field listed in details.field and retry. |
NO_VALIDATIONS_SELECTED | 400 | Add at least one validation id in validations[], or name a validationSetId from GET /api/validation-sets. |
ONE_VALIDATION_SET_PER_JOB | 400 | Send a single validationSetId string. A subject that needs a different set is a separate job. |
PARAMETER_NOT_CUSTOMIZABLE | 400 | A parameters key names a Standard-only parameter: its value is set by the validation's Standard and a job cannot change it. details.failures lists each validationId and parameterName refused. GET /api/validations publishes only the parameters a job may send, under customParameters. Refused before pricing, so nothing is charged and no job is created. |
PLAN_EXCLUDES_SELECTED_CHECKS | 400 | After plan gating, no selected validation remains runnable — a job that would do nothing is refused rather than accepted at a zero quote. details.gatedValidationIds lists what was excluded and details.missingFeatures the plan features that would unlock them. Nothing was charged and no job was created. |
SIGNUP_INVALID_EMAIL | 400 | Provide a syntactically valid email address in the `email` field. It becomes the account owner, so a human must be able to sign in with it later. |
UNKNOWN_EMAIL_CLIENT | 400 | A client id in clients does not exist in the rbtr render catalog at that form factor. details.validationId names the validation and details.unknownClients lists the (formFactor, clientId) pairs. GET /api/email-clients returns the current catalog. Refused before pricing, so nothing is charged and no job is created. |
UNKNOWN_FIELD | 400 | Remove or correct the fields in details.unknownFields. The accepted top-level fields are listed in the error message; the most common cause is a typo on a field that matters (e.g. "validation" for "validations"). |
VALIDATION_NOT_OFFERED | 400 | Use an id from GET /api/validations. |
VALIDATION_SET_CONFLICT | 400 | Send validationSetId, or an explicit validations[] list, not both with an internal validationSets array. Extra checks belong in validations[]. |
VALIDATION_SET_NOT_FOUND | 400 | Call GET /api/validation-sets (or GET /api/validation-sets/:setId) for published ids, or name checks in validations[]. |
VALIDATION_SET_SUBJECT_MISMATCH | 400 | validationSetId must match the job type: url jobs take url sets; email jobs take email sets. details.setSubjectType and details.jobSubjectType name the mismatch. |
VALIDATION_SUBJECT_MISMATCH | 400 | Every named validation must match the job type: url jobs take url-subject checks, email jobs take email-subject checks. details.mismatches names each offender with its subject type. Pick ids from GET /api/validations whose subjectType matches your job. |
AUTH_REQUIRED | 401 | Sign in and retry the request with a valid session. |
INSUFFICIENT_CREDITS | 402 | The organization does not hold enough credits for this job. Nothing was charged and no job was created. See details.balance, details.required and details.shortfall; quote first with POST /api/jobs/estimate. |
ADMIN_REQUIRED | 403 | Authenticate as an admin user before calling this endpoint. |
CUSTOMER_SCOPE_MISMATCH | 403 | Use the authenticated customer ID, or sign in as the correct customer. |
PLAN_EXCLUDES_EMAIL_CHECKS | 403 | The organization's plan does not include email checks. details.feature names the plan feature that unlocks them; upgrade the plan in the billing portal and retry. Nothing was charged and no job was created. |
PLAN_EXCLUDES_REQUESTED_CLIENTS | 403 | An explicitly named (formFactor, clientId) pair in clients is outside the organization's plan grants. Explicit requests are refused, never silently narrowed. details.validationId names the validation, details.refusedClients the pairs, and details.missingFeatures the plan feature(s) that unlock them. Nothing was charged and no job was created. |
PLAN_EXCLUDES_WEBSITE_CHECKS | 403 | The organization's plan does not include website checks. details.feature names the plan feature that unlocks them; upgrade the plan in the billing portal and retry. Nothing was charged and no job was created. |
UNAUTHORIZED_DOMAIN | 403 | Sign in with an email address on an invited domain. There is no self-serve allowlist to change: domain restriction is enforced by the identity provider for this deployment, so an unexpected refusal here is a support question. |
UNIVERSAL_CAPTURE_NEEDS_PRIVATE_INBOX | 403 | emailCapture.mode "universal" selects a message already in the organization's capture inbox by latest / subject / time window. Only paid plans have a private inbox; on Free every organization would share one, so a selector could match another customer's mail. Omit emailCapture (dedicated mode, the default) to get a one-time testEmail for this job, or upgrade the plan. Refused before pricing, so nothing is charged and no job is created. |
CONNECT_REQUEST_EXPIRED | 404 | Authorization requests live 10 minutes. The client must restart /oauth/authorize. |
CONNECTION_NOT_FOUND | 404 | GET /api/oauth/connections lists the caller’s live connections. |
CUSTOMER_NOT_FOUND | 404 | No organization matches that customerId. Omit it to act as the organization your credential belongs to — that is the default, and naming a different tenant is refused as CUSTOMER_SCOPE_MISMATCH rather than answered. |
JOB_NOT_FOUND | 404 | Verify the jobId and customer scope. |
STORAGE_OBJECT_NOT_FOUND | 404 | Check customerId, jobId, and storage key. Legacy jobs may use _screenshot. |
VALIDATION_NOT_FOUND | 404 | Use GET /api/validations for offered ids, or a published content slug from /validations/:slug/standard. |
VERDICT_TARGET_NOT_FOUND | 404 | No validation_results rows match (jobId, validationId) inside the caller's tenant. Verify both ids against GET /api/jobs/:jobId. |
ACCOUNT_EXISTS | 409 | A tenant already exists for this email address. POST /api/signup only provisions NEW accounts and never returns a credential for an existing one. Sign in at /auth/login and use POST /api/keys, or `npx @rbtrqa/cli login` for a CLI key. |
CONNECT_REQUEST_ANSWERED | 409 | Each authorization request takes exactly one decision. |
JOB_CHARGE_CONFLICT | 409 | A credit_ledger charge already exists for this job id under a different tenant or a different amount. Job ids are minted server-side and are not accepted in the request body, so this should be unreachable — treat it as an invariant violation and check the logs for billing.charge_conflict. Nothing was charged and no job was created. |
ORG_CANNOT_CONNECT | 409 | Connections bind only to ordinary customer organizations, never the platform or sign-up organizations. |
VERDICT_ALREADY_RECORDED | 409 | A verdict is immutable once recorded. Repeating the SAME verdict is accepted and updates the note; a different verdict is refused. details.recorded carries the stored verdict. |
INTERNAL_ERROR | 500 | Retry the request. If it persists, contact support with the requestId. |
JOB_EXECUTION_FAILED | 500 | The job failed after it was accepted and charged. Settlement refunds undelivered captures and any errored result rows, so a partial run is not billed in full. Retry if retryable is true; otherwise contact support with the requestId and jobId. |
BILLING_NOT_PROVISIONED | 503 | The organization has no billing customer with our billing provider, or that customer holds no live agreement, so no plan can be resolved and no default is substituted. Nothing on the caller side fixes it: contact support, and the repair happens in our provisioning. |
BILLING_PROVIDER_UNREACHABLE | 503 | The Kinde Management API or the synced plan catalog could not be read. No plan data is assumed during the outage — every request needing the plan fails until Kinde answers. Check Kinde status, credentials, and network egress; a cached resolution (≤5 min) is still served where one exists. |
EMAIL_CLIENT_CATALOG_UNAVAILABLE | 503 | The rbtr client catalog is unreachable and nothing is cached, so client ids in clients cannot be validated. Refusing beats skipping: an unvetted id would fail at capture time, after the charge. Retry, or resubmit without per-validation clients. Nothing was charged and no job was created. |
PLAN_NOT_PUBLISHED | 503 | The customer's Kinde agreement is missing a usable credits entitlement (absent, ≤0, or the 2147483647 unlimited sentinel on a paid plan) or the plan code is not one we grant for. Fix the plan's credits meter in the Kinde dashboard; Free ignores that field and uses FREE_MONTHLY_CREDITS. Requests needing the plan fail until the entitlement is set. |
SERVICE_UNAVAILABLE | 503 | A dependency this endpoint requires is unconfigured or unreachable, so the request was refused before any work or billing. This is our deployment, not your request: the body is unchanged and retrying is safe. Call sites override this message with the specific dependency; the request log carries it either way. |