Errors
One envelope for every failure, with a coarse class and a specific machine code
Every error response has the same shape, whatever went wrong.
{
"error": {
"type": "invalid_request_error",
"code": "FLOW_NOT_FOUND",
"message": "No flow with id flw_nope",
"param": "flow",
"request_id": "req_8a3f21bc"
}
}| Field | Notes |
|---|---|
type | A coarse class. Use it to decide how to react. |
code | A specific upper snake case machine code. Use it to decide what to say. |
message | Human-readable, for your logs. Do not show it to end users and do not parse it. |
param | The offending field, on validation errors. |
request_id | Also on the X-Request-Id response header. Quote it in support tickets. |
detail | Present only on the few errors you are meant to act on rather than log. It holds identifiers and dates, never applicant data. ALREADY_VERIFIED, VERIFICATION_IN_REVIEW and REVIEW_ALREADY_DECIDED are the ones that carry it today. |
Error types
| Type | Status | What it means and what to do |
|---|---|---|
invalid_request_error | 400, or 409 | Your request was malformed, referenced something that does not exist, or conflicts with a record that already exists. Fix the call. Never retry unchanged. Branch on code, not on the status: within this type, ALREADY_VERIFIED, VERIFICATION_IN_REVIEW, REVIEW_ALREADY_DECIDED and STALE_VERSION are the ones that arrive as 409. Other types return 409 too, so the status alone never tells you what happened. |
authentication_error | 401 | Missing, malformed or revoked key. Check the Authorization header. |
permission_error | 403 | The key is valid but may not do this. INSUFFICIENT_PERMISSIONS means a missing scope, see Permissions. READ_ONLY_KEY means the key cannot write at all and no scope will change that, see Read-only keys. |
not_found_error | 404 | No such resource for this account and environment. |
idempotency_error | 409 | The same Idempotency-Key was reused with a different body. See Idempotency. |
rate_limit_error | 429 | Too many requests. Back off and retry, see Rate limits. |
api_error | 5xx | Something failed on our side. Safe to retry with backoff. |
Which errors to retry
Retry rate_limit_error and api_error, with exponential backoff and jitter. Do not retry the 4xx
classes, since the same request will fail the same way. If you retry a create call for any reason,
send the same Idempotency-Key so you cannot accidentally create two verifications for one user.
A 409 saying ALREADY_VERIFIED
This applicant already holds an approval on this flow, so no session was created and nothing was
charged. It is a 409 rather than a 403 because nothing is wrong with your key or your request, it
conflicts with a record that already exists.
Refusing repeats is on for every account by default, so an integration that creates a verification on every sign-in will meet this. An owner can turn it off under Settings.
Never retry it unchanged. A retry succeeds only once the approval lapses, which may be a year away, so
a client with ordinary backoff will hammer the endpoint until then. Read detail.verification_id and
reconcile against that verification.
If the person genuinely has to go again, lift the refusal on that one approval and create the verification again:
curl -X POST https://api.trustwix.com/v1/verifications/vs_2f8a1c9b4e7d/reverification \
-H "Authorization: Bearer TWK_..." \
-H "Content-Type: application/json" \
-d '{ "reason": "document_expired" }'It takes the same verification:create permission as the create call itself, so the key you already
use needs nothing added to it. An owner can also do it from that verification's page in the console.
Letting one person verify again
has the full resource, and Check existing
applicants covers the whole
control.
A 409 saying VERIFICATION_IN_REVIEW
This applicant already has a verification waiting on manual review with you, so no session was
created and nothing was charged. It is governed by the same setting as ALREADY_VERIFIED and matched
the same way, and it exists for the same reason: a second attempt started while the first is still in
the queue is how one person ends up approved twice.
Do not retry it in a loop. It keeps being refused until a reviewer decides the open case, and that
decision arrives on the open verification, not on a new one. Read detail.verification_id, which is
that open case, and detail.matched_on, and wait for its outcome. If the review ends in an approval,
the person is verified; if it ends in a rejection, a new verification is accepted again. See While the
person is waiting on
review.
A 409 saying REVIEW_ALREADY_DECIDED
Somebody decided that review before you did, so your decision was not recorded and the case keeps the answer they gave it.
This is not an edge case to defend against, it is the normal result of two people working one queue. The claim behind a decision is a single conditional write, so when your back office and a reviewer in the console both act on a case, exactly one of them wins and the other reads this. If you have built review into your own admin, expect it in ordinary traffic.
Never retry it. A decided case stays decided, so backoff will not help. Read detail.decision and
detail.reviewed_at and show the outcome that was actually reached, which is almost always what your
operator needed to know. It is a far better screen than an error.
Who decided is not in the payload. That person is one of your own team, and the console names them on the case.
A 403 saying READ_ONLY_KEY
The key is read-only and the endpoint you called writes. Nothing is wrong with your scopes.
Do not try to fix this by granting a permission. Read-only outranks every scope on the key and is fixed for its lifetime, so granting one changes nothing, which is exactly the property that makes a read-only key worth handing out. Point the call at a read-write key, or create one.
The usual cause is a job holding the wrong credential. GET /v1/entitlements reports access for
whichever key you are currently using, which is the quickest way to confirm it.
A 409 saying STALE_VERSION
Somebody edited that configuration between your read and your write, so nothing was applied.
Read the resource again, show whoever is editing what changed underneath them, and resubmit with the
new version. error.detail.current_version tells you what it is now.
Never retry this automatically. A client that retries a conflict blindly has reimplemented last-write-wins on top of the control that exists to stop it. See Configuration.
A 403 in production that passed in staging
A live key carries a narrower default permission set than a sandbox one, so a handful of reads work
all the way through your tests and then return permission_error on the first production deploy.
Permissions lists which, and why. Check that before you go looking for
a bug.
A cross-environment call is not a 404 by accident
Sandbox and live data are separate. Reading a live verification with a sandbox key returns
not_found_error rather than the record, which is deliberate. If a lookup that works in production
returns 404 in staging, check which environment your key belongs to before you check anything else.
Codes are additive
New code values can appear within the existing type classes. Branch on type for control flow
and treat code as a refinement, with a default branch for codes you have never seen.