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.

CodeStatusMeaning
CAPTURE_PROHIBITED_HERE400A (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_JOB400clients 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_CHECK400clients 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_PARAMETERS400Two 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_VALIDATION400The 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_SELECTION400clients 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_SELECTION400clients 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_VALUE400Use one of the allowed values listed in details.allowedValues.
INVALID_PARAMETER_VALUE400Check the submitted value against the Parameter format requirements returned in details.
INVALID_REQUEST_BODY400Check the request schema, required fields, content type, and JSON formatting.
JOB_ACCEPTS_ONE_SUBJECT400POST /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_REQUIRED400Supply 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_SHAPE400The 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_FIELD400Add the missing field listed in details.field and retry.
NO_VALIDATIONS_SELECTED400Add at least one validation id in validations[], or name a validationSetId from GET /api/validation-sets.
ONE_VALIDATION_SET_PER_JOB400Send a single validationSetId string. A subject that needs a different set is a separate job.
PARAMETER_NOT_CUSTOMIZABLE400A 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_CHECKS400After 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_EMAIL400Provide 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_CLIENT400A 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_FIELD400Remove 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_OFFERED400Use an id from GET /api/validations.
VALIDATION_SET_CONFLICT400Send validationSetId, or an explicit validations[] list, not both with an internal validationSets array. Extra checks belong in validations[].
VALIDATION_SET_NOT_FOUND400Call GET /api/validation-sets (or GET /api/validation-sets/:setId) for published ids, or name checks in validations[].
VALIDATION_SET_SUBJECT_MISMATCH400validationSetId 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_MISMATCH400Every 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_REQUIRED401Sign in and retry the request with a valid session.
INSUFFICIENT_CREDITS402The 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_REQUIRED403Authenticate as an admin user before calling this endpoint.
CUSTOMER_SCOPE_MISMATCH403Use the authenticated customer ID, or sign in as the correct customer.
PLAN_EXCLUDES_EMAIL_CHECKS403The 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_CLIENTS403An 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_CHECKS403The 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_DOMAIN403Sign 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_INBOX403emailCapture.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_EXPIRED404Authorization requests live 10 minutes. The client must restart /oauth/authorize.
CONNECTION_NOT_FOUND404GET /api/oauth/connections lists the caller’s live connections.
CUSTOMER_NOT_FOUND404No 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_FOUND404Verify the jobId and customer scope.
STORAGE_OBJECT_NOT_FOUND404Check customerId, jobId, and storage key. Legacy jobs may use _screenshot.
VALIDATION_NOT_FOUND404Use GET /api/validations for offered ids, or a published content slug from /validations/:slug/standard.
VERDICT_TARGET_NOT_FOUND404No validation_results rows match (jobId, validationId) inside the caller's tenant. Verify both ids against GET /api/jobs/:jobId.
ACCOUNT_EXISTS409A 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_ANSWERED409Each authorization request takes exactly one decision.
JOB_CHARGE_CONFLICT409A 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_CONNECT409Connections bind only to ordinary customer organizations, never the platform or sign-up organizations.
VERDICT_ALREADY_RECORDED409A 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_ERROR500Retry the request. If it persists, contact support with the requestId.
JOB_EXECUTION_FAILED500The 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_PROVISIONED503The 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_UNREACHABLE503The 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_UNAVAILABLE503The 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_PUBLISHED503The 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_UNAVAILABLE503A 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.