TrustWixDocs

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"
  }
}
FieldNotes
typeA coarse class. Use it to decide how to react.
codeA specific upper snake case machine code. Use it to decide what to say.
messageHuman-readable, for your logs. Do not show it to end users and do not parse it.
paramThe offending field, on validation errors.
request_idAlso on the X-Request-Id response header. Quote it in support tickets.
detailPresent 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

TypeStatusWhat it means and what to do
invalid_request_error400, or 409Your 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_error401Missing, malformed or revoked key. Check the Authorization header.
permission_error403The 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_error404No such resource for this account and environment.
idempotency_error409The same Idempotency-Key was reused with a different body. See Idempotency.
rate_limit_error429Too many requests. Back off and retry, see Rate limits.
api_error5xxSomething 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.

On this page