Errors
Last verified: 2026-06-20 (Task #389 — Visa Intelligence API docs).
Every Visa Intelligence endpoint returns the same flat error
envelope. There is no nested error object — error is the message and
code is a stable machine token at the top level.
{
"error": "API key is missing required scope: visa:check",
"code": "visa_api_key_insufficient_scope",
"requiredScope": "visa:check"
}
| Field | Always present | Notes |
|---|
error | yes | Human-readable message. Don't match on this — it can change. |
code | yes | Stable token to branch on. |
details | no | Structured context (varies by error). |
requiredScope | no | The scope you were missing (on scope 403s). |
retryAfter | no | Seconds to wait (on 429s). |
Treat code as the contract. The error text is for humans and may be
reworded.
Authentication & authorization
| Code | HTTP | Meaning |
|---|
visa_api_key_unconfigured | 503 | No key presented and no key configured on this deployment. |
visa_api_key_invalid | 401 | Missing key, or the key doesn't resolve. |
visa_api_key_revoked | 401 | Key was revoked. |
visa_api_key_expired | 401 | Key's expires_at has passed. |
visa_api_customer_inactive | 403 | The tenant behind the key is disabled. |
visa_api_key_insufficient_scope | 403 | Key valid but missing the endpoint's scope (see requiredScope). |
Rate limiting
| Code | HTTP | Meaning |
|---|
visa_api_rate_limited | 429 | Per-key hourly quota exceeded. retryAfter + Retry-After tell you when to retry. See rate-limits.md. |
Request validation
| Code | HTTP | Meaning |
|---|
invalid_json | 400 | Body wasn't valid JSON. |
invalid_request | 400 | Body/params failed schema validation. details may carry field-level issues. |
visa_changes_bad_query | 400 | A /v1/visa/changes query parameter was malformed (e.g. an unknown severity). |
not_found | 404 | Resource not found, or not visible to your key (e.g. document download). |
Webhooks
| Code | HTTP | Meaning |
|---|
visa_webhook_master_key_forbidden | 403 | The development master key tried to manage webhooks. Use a customer key. |
visa_webhook_url_blocked | 400 | The target URL isn't https:// or resolves to a private/loopback/disallowed address (details.reason). |
visa_webhook_not_found | 404 | No webhook with that id belongs to your tenant. |
Server
| Code | HTTP | Meaning |
|---|
internal_error | 500 | Unhandled server error. Safe to retry with backoff. |
Handling errors in code
Branch on code, not HTTP status alone or message text:
const res = await fetch("https://api.travelmode.ai/v1/visa/check", {
method: "POST",
headers: {
"X-Visa-Intel-Key": process.env.VISA_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ nationalityCode: "US", destinationCountryCode: "FR" }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
switch (body.code) {
case "visa_api_rate_limited":
// wait body.retryAfter seconds, then retry
break;
case "visa_api_key_insufficient_scope":
throw new Error(`key needs scope: ${body.requiredScope}`);
case "visa_api_key_invalid":
case "visa_api_key_expired":
case "visa_api_key_revoked":
throw new Error("re-issue your API key");
default:
throw new Error(`visa API error ${res.status}: ${body.code ?? "unknown"}`);
}
}