Skip to content
instantscan.io docs

Errors

The error envelope, every status code the API returns, what each error code actually means, and which ones are worth retrying.

The envelope

Every failure is JSON with a stable, machine-readable error string:

{
  "error": "invalid_request",
  "message": "Human-readable detail. Safe to log, not safe to show a user verbatim.",
  "fields": { "theme.accent": "must be a CSS colour, e.g. #1a90cc or rgb(26 144 204)" }
}
  • error — branch on this. It does not change without a version bump.
  • message — present on most errors. Written for your logs.
  • fields — present on validation failures only. Keys are field paths, so a form can attach each message to the right input.

Status codes

StatusMeaningRetry?
400The request is malformed or a field is invalid.No — fix the request.
401The key or session is missing, wrong, or revoked.No.
402Out of credits.After topping up.
403Authenticated but not permitted: origin not allowlisted, or a bad URL signature.No.
404No such object, or it belongs to another tenant.No.
409Conflict: an already-used client token, or an email that is taken.No.
410The session expired.No — create a new session.
413The upload is larger than the limit (25 MB by default).No.
429Rate limited.Yes, with backoff.
5xxOur fault.Yes, with backoff.

404 covering “belongs to someone else” is deliberate. Returning 403 there would confirm that an id exists, which is an enumeration oracle.

Error codes

Authentication and keys

CodeStatusCause
invalid_api_key401The Authorization: Bearer … header is absent, malformed, unknown, or the key was revoked.
secret_key_required401The endpoint needs an sk_ key and you sent a pk_. Reading a session is the usual case.
invalid_credentials401Console login failed. Identical for an unknown email and a wrong password, on purpose.
unauthorized401No valid console session cookie on a /v1/dashboard/* route.
email_taken409Signup with an email that already has an account.

Sessions and scanning

CodeStatusCause
origin_not_allowed403A pk_ key was used from an origin not on your allowlist, or allowed_origins named an origin that is not.
insufficient_credits402Zero balance. No session was created.
invalid_scanner_config400The scanner override failed validation. See fields. No session was created.
invalid_token401 / 400The client token is malformed, tampered with, or expired.
session_expired410The session passed its TTL (15 minutes by default).
token_already_used409The client token already uploaded a document. One session, one document.
empty_file400The upload had no bytes.
file_too_large413Above MAX_UPLOAD_BYTES.
invalid_signature403A file URL’s sig or exp does not verify.
rate_limited429Too many session creations in the window.

Console

CodeStatusCause
invalid_request400Validation failed. Always accompanied by fields.
not_found404No such key, domain or delivery — or it is another tenant’s.
no_endpoint400Sending a test webhook with no endpoint configured.

Which errors to retry

Retry with exponential backoff: 429 and 5xx. Nothing else.

Never retry: 400, 401, 403, 409, 410, 413. The same request will fail identically, and retrying a 402 without topping up just wastes calls.

A special case — 410 session_expired: do not retry the upload. Create a new session and let the user scan again. That costs another credit, which is worth surfacing in your own metrics.

async function createSession(body: unknown, attempt = 0): Promise<Response> {
  const res = await fetch("https://api.instantscan.io/v1/sessions", {
    method: "POST",
    headers: { Authorization: `Bearer ${SECRET_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });

  const retryable = res.status === 429 || res.status >= 500;
  if (retryable && attempt < 3) {
    await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
    return createSession(body, attempt + 1);
  }
  return res;
}

Errors in the browser

The SDK never rejects with a raw HTTP error. Everything becomes a ScannerError with a code of cancelled, config, network or scan_error — see Embed SDK → Errors.

The important one: cancelled is not a failure. A user changing their mind is normal, and treating it as an error is the most common integration bug we see.

Errors in webhook delivery

If your endpoint returns non-2xx or times out, the delivery is retried up to three times with backoff. Every attempt is recorded with its response status, duration and transport error, visible in Webhooks → Deliveries.

There is no dead-letter queue. When attempts are exhausted the delivery is marked failed and stays in the log, where you can inspect the exact signed body and replay it once your endpoint is fixed.

Debugging checklist

SymptomFirst thing to check
403 origin_not_allowed in the browserIs the exact origin — scheme, host and port — on your allowlist? http://localhost:3000 and http://127.0.0.1:3000 are different origins.
401 invalid_api_key on a working keyWas it revoked? Are you sending a live key to a test-mode integration, or vice versa?
409 token_already_usedYou are reusing a scan_url. Create one session per document.
Scanner opens then immediately errorsCheck GET /v1/scanner/config?t=… directly. A 410 means the session expired before the user got there.
Webhook signature never matchesYou are verifying a re-serialised body instead of the raw bytes.
Nothing at all happens on clickLook for a ScannerError in the console. Almost always config (session refused) or network (wrong apiBase).