Skip to content

Errors

Base URL in examples: https://api.ordainer.dev.

An agent key is the key you issue to each integration or AI agent.

A deny is not an error. allow and every deny (quota_exceeded, tool_not_in_plan, test_limit_reached) come back as HTTP 200 with a decision field. See Deny reasons.

Every error from the API is a non-200 response with this body:

{
"error": {
"code": "revoked_key",
"message": "This agent key has been revoked.",
"request_id": "req_TZWx7iYiHQ4l6vZX"
}
}
  • Branch on error.code. The message wording may change.
  • The X-Request-Id response header always equals error.request_id. Quote it to support.
  • Requests that end in an error write no audit row and never move a counter. Our server log does keep one line per 4xx (request ID, status, error code and organization, and on a 429 which limit fired, limit_scope; never your key, address, body, headers or query), so support can trace an errored request from its request ID.
  • Checks run in this order, so you only see the first problem: database configured → Authorization present → old key format → key format → per-key rate limit → recently seen unknown or revoked key → per-key concurrency → body size and JSON → fields → key lookup → revoked → subscription → decision. A missing, malformed or unknown key normally gets its 401; once your address has sent too many of those, it gets 429 too_many_failed_auth instead. A valid key is never refused by that limit, and an old sk_ key (401 old_key_format) doesn’t count toward it.
  • New codes can be added within /v1. From launch on, codes are only added, never renamed (Versioning). If you get a code you don’t know, act on the HTTP status: 4xx = fix the request or the account, 429 = wait Retry-After (and if limit_scope is per_address, fix the key first), 5xx = retry with the same idempotency key.

Errors from the Node SDK (this page describes SDK 1.2.4, on npm as ordainer) come in two kinds: the response the SDK sends your caller when it blocks a call (SDK blocked responses), and the error.code on a result when the SDK couldn’t get a decision (SDK result codes). Every API error below (any 4xx) is blocked by the SDK in every failMode, except subscription_inactive, which follows the SDK’s separate billing-lapse setting billingLapse (default 'open'), never failMode, and safety_limit_reached, which runs the call in every failMode (SDK 1.2.0); otherwise only a timeout, network error, 5xx or unreadable answer follows failMode. The Ruby gem follows the same rules (see its fail_mode and billing_lapse settings). The plain HTTP examples in other languages just print error.code.

Code HTTP Where it comes from Retry?
bad_request 400 Any path (the request target isn’t a valid path) After fixing the client
invalid_json 400 API, demo After fixing the request
invalid_request 400 API, demo, console, SDK (client-side) After fixing the request
invalid_idempotency_key 400 API After fixing the key
missing_key 401 API, SDK (client-side) After adding the key
unknown_key 401 API No
old_key_format 401 API (an agent key from before the rename, sk_test_… / sk_live_…) No, create a new ord_ key
revoked_key 401 API No
subscription_inactive 403 API (live keys) No: pay the open invoice by day 30 after the missed renewal (same keys back), else resubscribe
subscription_required 403 (API) / 402 (console) API (live keys), console No, subscribe first
live_keys_unavailable 403 Console only (creating or reissuing a live key) No, use test keys
cross_customer_move 400 Console only (moving or creating a key) No, pick another plan
csrf_failed 403 Console and sign-in only After reloading the page
conflict 409 Console only (adding a customer) After changing the external id
subscription_active 409 Console only (deleting the account) After the subscription has ended
billing_not_configured 503 Console only (Billing page) No, contact support
not_found 404 API No
method_not_allowed 405 API No
idempotency_conflict 409 API No, use a new idempotency key
payload_too_large 413 API, demo After fixing the request
rate_limited 429 API. About 100 requests a second, each server counts separately. limit_scope is "per_key". Demo is per address, per hour. SDK blocked response (any failMode) Yes, after Retry-After
concurrency_limited 429 API (per agent key: more than 4 requests in flight at once on our hosted service, with up to 20 more waiting briefly; limit_scope: "per_key_concurrency") Yes, after Retry-After: 1, with fewer calls at once
safety_limit_reached 429 API (shadow calls on test keys past 10 times the organization’s monthly test limit; limit_scope: "org_safety") After resets_at (the start of next month, UTC)
too_many_failed_auth 429 API (per address: too many missing, unknown or malformed keys; limit_scope: "per_address"; a valid key is refused only in the rare case its check couldn’t run) After fixing the key
internal_error 500 API, demo Yes, with the same idempotency key
db_timeout 503 API, demo Yes, after Retry-After: 1, with the same idempotency key
decide_timeout 503 API Yes, after Retry-After: 1, with the same idempotency key
db_unavailable 503 API Yes, after Retry-After: 1, with the same idempotency key
db_not_configured 503 Self-hosted/local servers only After fixing the server
demo_unavailable 503 Self-hosted/local servers only (demo endpoint) After fixing the server
test_limit_reached 200 deny (API) / 429 (SDK) API deny reason, SDK blocked response After the 1st of next month (UTC)
plan_limit_reached 429 (or denyStatus) SDK blocked response After resets_at
plan_denied 403 SDK blocked response No
plan_check_rejected 403 (billing) / 500 (setup) SDK blocked response only, never from the API (an API error in any failMode, no key or tool, or a lapsed subscription with billingLapse: 'closed') No: the fix is on the vendor’s side (billing, or the key and setup)
ordainer_unavailable 503 SDK blocked response (no answer from Ordainer with fail closed) Yes, after Retry-After: 5
timeout none SDK result code Yes
network_error none SDK result code Yes
server_error none SDK result code (5xx with no error body only; a normal 500 is internal_error) Yes
bad_response none SDK result code Maybe

What it means: the request target (the path in the request line) is not a valid URL path, e.g. // or http://. Nothing ran.

{"error":{"code":"bad_request","message":"The request target is not a valid path.","request_id":"req_Q2s8YdLw0FhT3kVb"}}

How to fix it: fix the client that sent it.

Retry? Only after fixing the client. The same request fails the same way.

What it means: the request body isn’t valid JSON.

{"error":{"code":"invalid_json","message":"Request body is not valid JSON.","request_id":"req_Vj3gwPsP7n1Sso4O"}}

How to fix it: send a JSON object such as {"tool":"research.run"}. In shells, single-quote the body so the shell doesn’t eat the double quotes. Build the body with your JSON library rather than by string concatenation.

Retry? Only after fixing the body. The same request fails the same way.

What it means: the body parsed, but something in it is wrong. message says what:

message Cause
Request body must be a JSON object. The body is an array, a string, a number or null.
"tool" is required: a string of 1-200 characters (surrounding whitespace is ignored). tool is missing, not a string, empty (or only spaces), or over 200 characters after trimming. An empty body counts as {}.
"mode" must be "enforce" or "shadow" (omit it to use the plan's mode). mode is any other value.

Demo only: POST /demo/decide also returns invalid_request with key must be "pilot" or "prod".

SDK (client-side): decide() without a tool string returns a result with error.code: "invalid_request" (tool is required.) without calling the API. It’s blocked in every failMode (result.rejected: true).

Console: a form field out of range (for example a plan’s allowance) is also invalid_request, shown on an error page. See Console errors.

How to fix it: correct the field named in message.

Retry? Only after fixing the request.

What it means: the idempotency key isn’t a string of 1 to 255 characters: it’s empty (""), longer than 255, or not a string (for example a number). This applies to the body field idempotency_key, and to the Idempotency-Key header when the body field is absent or null. An empty key is an error, not “no key”. (Before launch this was reported as invalid_request.)

{"error":{"code":"invalid_idempotency_key","message":"\"idempotency_key\" must be a string of 1-255 characters (it was empty; omit it to send no key).","request_id":"req_KWbYjrprBLCLYacA"}}

The header version says The Idempotency-Key header must be a string of 1-255 characters.

How to fix it: send a key of 1 to 255 characters, built from your job or step id plus the tool. If you have no key, leave the field and header out (or send null in the body).

SDK: the Node SDK never sends an over-long key: if the key would be longer than 255 characters, it sends <tool>:<sha256 of your key> instead (deterministic, so retries still replay). An empty key isn’t sent at all (the SDK uses a random one). So from the SDK you’d only see this code if you pass a non-string key; like any 4xx it’s then blocked in every failMode (500 plan_check_rejected, result.rejected: true).

Retry? Only after fixing the key.

What it means: there’s no Authorization: Bearer … header, or it isn’t in the Bearer <key> form.

{"error":{"code":"missing_key","message":"Send your agent key as \"Authorization: Bearer ord_...\".","request_id":"req_EWJ4qahPhrssV5a5"}}

SDK (client-side): if no key was passed and the key environment variable isn’t set, the SDK doesn’t call the API. The result has error.code: "missing_key" and the call is blocked in every failMode (500 plan_check_rejected, result.rejected: true), with a [ordainer] rejected (call blocked in any failMode) warning in your log.

How to fix it: send Authorization: Bearer ord_test_… or ord_live_…. The word Bearer isn’t case-sensitive.

Counts toward the per-address limit: each request with a missing key uses up one of the 30 failed-key requests your address may send per minute. Past that, requests with a missing or unknown key get 429 too_many_failed_auth instead of the 401. Requests with a valid key are not affected.

Retry? After adding the header.

What it means: the key doesn’t match any key we know. Keys from the public demo are rejected here on purpose. When the value isn’t shaped like an agent key at all (a typo, a cut-off copy, quotes or a placeholder), you get the same code straight away, without a lookup, and the message adds what a key looks like: ord_test_ or ord_live_ followed by 32 letters, digits, - or _ (second example below). An agent key from before the rename (sk_test_ or sk_live_ followed by exactly 32 such characters) gets 401 old_key_format instead. A key we just looked up and didn’t find is answered from memory for about a minute. A key you create later is never affected: new keys are random, so they can’t match one of those.

{"error":{"code":"unknown_key","message":"This agent key is not recognized.","request_id":"req_Wagqndw7Q6ox7lY-"}}

A value that isn’t shaped like a key:

{"error":{"code":"unknown_key","message":"This agent key is not recognized. An agent key is ord_test_ or ord_live_ followed by 32 letters, digits, - or _ (41 characters in all). Copy it again rather than retyping it.","request_id":"req_…"}}

How to fix it: most often, the key was typed or copied by hand and is a character off. Keys are shown once and we store only a hash, so a mistyped or lost key can’t be recovered. Create a new key in Console → Keys and copy it with its Copy button. Also check for a stray space, and that you’re calling the right base URL (a local server and the hosted service have different keys).

SDK: an error answer, so the call is blocked in every failMode (500 plan_check_rejected, result.rejected: true, a [ordainer] rejected … X-Request-Id=… warning).

Counts toward the per-address limit: each request with an unknown (or wrongly shaped) key uses up one of the 30 failed-key requests your address may send per minute. Past that, requests with a missing or unknown key get 429 too_many_failed_auth instead of the 401. Requests with a valid key from the same address keep working, so one service with a bad key can’t block your other services. A revoked key doesn’t count (that’s revoked_key), and neither does an old sk_ key (old_key_format).

Retry? No. It will fail the same way.

What it means: the key is an agent key from before the product rename: sk_test_ or sk_live_ followed by exactly 32 letters, digits, - or _. Those keys no longer work anywhere. Agent keys now start with ord_test_ or ord_live_. The answer comes straight away, before any lookup. A string that starts with sk_ but has a different length isn’t an old key: it gets unknown_key.

HTTP/1.1 401 Unauthorized
X-Request-Id: req_xNsg4kchlCq4_PXO
{"error":{"code":"old_key_format","message":"This key uses an old format. Create a new key in the dashboard.","request_id":"req_xNsg4kchlCq4_PXO"}}

There is no Retry-After, no limit_scope and no other hint field: the body is the usual code, message and request_id.

How to fix it: create a new key in Console → Keys (ord_test_… for testing, ord_live_… for production) and replace the old one everywhere it’s set (environment, deploy config, secrets). If you use the Node SDK, the environment variable is now ORDAINER_KEY.

Does not count toward the per-address limit: an old key never uses up your address’s failed-key budget, so an agent still set up with one can’t push your other services into 429 too_many_failed_auth. It keeps getting this 401 even when the address is over that limit.

SDK: an error answer like any other 401: the call is blocked in every failMode (500 plan_check_rejected, result.rejected: true, result.error.code: 'old_key_format'), with a [ordainer] rejected (call blocked in any failMode): old_key_format: This key uses an old format. Create a new key in the dashboard. X-Request-Id=… warning.

Retry? No. Replace the key first.

What it means: the key was revoked in Console → Keys. Revocation takes effect on the next call.

{"error":{"code":"revoked_key","message":"This agent key has been revoked.","request_id":"req_TZWx7iYiHQ4l6vZX"}}

How to fix it: create a new key for that customer and plan, and replace the revoked one. A revoked key can’t be un-revoked. A revoked key is a real key, so it doesn’t use up your address’s failed-key budget.

SDK: blocked in every failMode (500 plan_check_rejected, result.rejected: true).

Retry? No.

What it means: this is a live key whose live keys are off, in one of two states:

  • Payment cutoff, comeback period. A renewal payment failed and was still unpaid at the cutoff (day 14 after the missed renewal). The subscription isn’t canceled yet: until day 30 after the missed renewal you can pay the open invoice and the same live keys turn back on by themselves.
  • Subscription ended. It was canceled and the paid period is over, it was canceled while a payment was unpaid, the comeback period ended (day 30) unpaid, or the first payment never completed. These keys are retired: they keep returning this error and never work again.

While a failed renewal is still being retried (before the cutoff), keys keep working. Test keys keep working until the account is deleted, with its audit history, in one step 90 days after the subscription ends (day 120 after a missed renewal that is never paid). A key that was also revoked returns revoked_key instead (revocation is checked first).

{"error":{"code":"subscription_inactive","message":"This live key is inactive because the vendor's Ordainer subscription has ended. Test keys still work. …","request_id":"req_8NKtWea13EyalB74"}}

(The last sentence says what brings live keys back: during the comeback period, paying the open invoice turns the same key back on; after the subscription ended, the key is retired and you subscribe again and issue new keys. Branch on the code, not the message.)

How to fix it:

  • Comeback period (to day 30): pay the open invoice from Console → Billing → Manage billing, or the link in our email. The same keys come back within a minute; you don’t need new keys.
  • After the subscription ended: subscribe again in Console → Billing (same account, your customers and plans are kept), then Console → Keys → Issue new live keys. It replaces each retired key with a new one (same label, customer and plan) and shows each new secret once. Put the new secrets in your tools; the old keys never work again.

SDK: treated as a billing state, not an outage. It follows the SDK’s billing-lapse setting (billingLapse, Terms 10.3(b)), not failMode. With billingLapse: 'open' (the default, whatever failMode is) the call runs, unchecked and uncounted (result.allowed: true, result.billingLapse: 'open'). With 'closed' (opt-in) your caller gets 403 plan_check_rejected with no Retry-After and a neutral message (result.rejected: true). In both modes it logs its own warning at most once a minute (“Calls are being let through, unchecked, because billingLapse is ‘open’ (the default)” or “Calls are being blocked (403 plan_check_rejected) because billingLapse is ‘closed’”), sets result.billingInactive: true and result.reason: "subscription_inactive", and calls onBillingInactive(result) on every such call.

Retry? No, not until the subscription is active again.

subscription_required · 403 (API) · 402 (console)

Section titled “subscription_required · 403 (API) · 402 (console)”

What it means: live keys need a paid subscription (there is no trial; the first month is charged when you subscribe). You see this code in two places:

  • /v1/decide, 403: a live key of an organization that has no paid subscription. Only live keys created before pay-first could be in this state. Test keys keep working.
  • Console, 402: trying to create a live key in Console → Keys without a paid subscription. The console shows this as a page, not JSON.
{"error":{"code":"subscription_required","message":"This live key is inactive because the vendor has no paid Ordainer subscription yet. Test keys still work. Subscribing (Console > Billing) activates live keys.","request_id":"req__0-6gN0SywqVKJxb"}}

How to fix it: subscribe in Console → Billing. Suspended live keys are activated automatically; you don’t need new keys.

SDK: blocked in every failMode, like other API errors (403 plan_check_rejected with no Retry-After, result.rejected: true), with the billing extras: result.billingInactive: true, result.reason: "subscription_required", its own warning starting [ordainer] subscription required: (“… Calls with this live key are blocked in any failMode (test keys still work). Subscribe in the Ordainer console under Billing.”), onBillingInactive(result).

Retry? No, not until the organization has subscribed.

What it means: no route matches the path.

{"error":{"code":"not_found","message":"No route for POST /v1/nope.","request_id":"req_clZNma3kJ5CLsJJ9"}}

How to fix it: check the path (/v1/decide) and the base URL. The Node SDK appends /v1/decide to baseUrl, so don’t include it there (it would call /v1/decide/v1/decide).

Retry? No.

What it means: the path exists, but not for this HTTP method. This includes OPTIONS: there is no CORS, on purpose, so browser preflight requests fail.

{"error":{"code":"method_not_allowed","message":"GET is not allowed on /v1/decide.","request_id":"req_K5N2LmHAUZF6dLAP"}}

How to fix it: use POST for /v1/decide. Call the API from your server, never from a web page: an agent key in a browser is a leaked key.

Retry? No.

What it means: this idempotency key was already used, with this agent key, for a different request: a different tool, or a different mode (sending mode on one call and not on the other counts as different). The original answer is not returned and nothing is counted. The same key, same tool and same mode is a normal replay: 200 with Idempotent-Replayed: true. Spaces around tool are trimmed first, so " research.run" and research.run are the same tool. (Before launch this code was idempotency_key_reused and covered only a different tool; it was renamed on purpose. From launch on, codes are never renamed.)

Different tool:

{"error":{"code":"idempotency_conflict","message":"This idempotency key was already used by this agent key with tool \"research.run\" (this request: \"other.tool\"), in decision req_BklTYW25gC0ukvaz. Use a new idempotency key for a different request.","request_id":"req_4sqNPyFBsYRg6WiC"}}

Different mode on a retry (the first call sent no mode, the retry sent "mode":"shadow"):

{"error":{"code":"idempotency_conflict","message":"This idempotency key was already used by this agent key with no mode (plan's mode) (this request: mode \"shadow\"), in decision req_BklTYW25gC0ukvaz. Use a new idempotency key for a different request.","request_id":"req_4M21nhLQi971rsg8"}}

The message names the earlier decision’s request_id, so you can look it up in Console → Decisions.

How to fix it: use one key per distinct call: for example job id + step + tool. A real retry must send the same tool and the same mode (or no mode both times). Build keys from your own data, not from a value your caller sends (the SDK middleware never forwards an incoming Idempotency-Key header). A key replays for 24 hours; after that it’s a new decision and this conflict can’t happen. Keys are per agent key, so the same string on another agent key never conflicts.

SDK: an error answer, so the call is blocked in every failMode (500 plan_check_rejected, result.rejected: true). From the SDK it means the code that builds idempotency keys reuses one key for different calls (include the tool in the key); retrying with the same key gets the same 409.

Retry? No, not with the same key. Use a new key for a different request.

What it means: the request body is over 16,384 bytes.

{"error":{"code":"payload_too_large","message":"Request body must be under 16384 bytes.","request_id":"req_QIWWY7p3R8bqDwXw"}}

How to fix it: send only tool, idempotency_key and mode. Don’t forward your tool’s own payload.

Retry? After shrinking the body.

What it means: too many requests in a short time with one agent key. No decision was made and nothing was counted or audited. Every 429 has a Retry-After header (whole seconds).

  • /v1/decide: About 100 requests a second, each server counts separately. Over it, the next calls get 429 with Retry-After (at least 1 second). The body has code rate_limited and "limit_scope": "per_key". (Too many requests at the same moment on one key is a separate limit: concurrency_limited.) The limit is counted on each of our servers separately, so the exact point where it starts is approximate: don’t rely on a precise threshold. Nothing is shared between servers. It’s checked before the key is looked up, so a well-formed key we don’t know is limited too. The message starts “This agent key is sending more than …”. Captured on a local copy of main with the limit lowered to 1 a second, so it says “about 1 request a second”. With the default it says “about 100 requests a second, each server counts separately”.
HTTP/1.1 429 Too Many Requests
X-Request-Id: req_RWmkmD3Q6X9jhGjJ
Retry-After: 1
{"error":{"code":"rate_limited","message":"This agent key is sending more than about 1 request a second, each server counts separately, so we didn't run this request. Please slow down and retry after 1s. No decision was made and nothing was counted. (rate_limited)","limit_scope":"per_key","request_id":"req_RWmkmD3Q6X9jhGjJ"}}

limit_scope is on every /v1/decide 429 and says which limit fired: per_key here, per_key_concurrency on concurrency_limited, per_address on too_many_failed_auth. Branch on error.code; limit_scope is there so a log or a generic 429 handler can tell the two apart. It is a string and only appears on error bodies. Don’t confuse it with a decision’s limit (a number, or null for no cap), which only appears on 200 answers.

  • Public demo (POST /demo/decide): per address, 60 calls an hour by default. Retry-After is the seconds left in the current hour. No limit_scope. Captured with the limit lowered to 2:
HTTP/1.1 429 Too Many Requests
X-Request-Id: req_NTQNH_ojnCbKjTHb
Retry-After: 1343
{"error":{"code":"rate_limited","message":"Too many demo calls from this address. Try again later.","request_id":"req_NTQNH_ojnCbKjTHb"}}

SDK: a rate limit, not an outage. The SDK blocks the call in every failMode, with its own 429 rate_limited. It’s never silent: result.rateLimited is true, result.error.limitScope is 'per_key', result.retryAfterSec has the wait, onRateLimited(result) runs on every such call, and a [ordainer] rate limited: this agent key is over Ordainer's per-key request limit … warning is logged at most once a minute.

How to fix it: /v1/decide: slow that key down, back off for Retry-After seconds, then retry with the same idempotency key. Check for a retry loop that doesn’t wait. If one key legitimately needs more, contact support. Demo: wait, or sign up for your own test keys.

Retry? Yes, after Retry-After seconds, with the same idempotency key.

What it means: this agent key already had 4 requests to /v1/decide in flight at once on our hosted service, on the server that took this one, and no slot freed up in time, so the request was refused. No decision was made and nothing was counted or audited. Up to 20 more requests for the same key wait briefly for a free slot; past the queue, or when the wait runs out, they get this 429. A request that waited in line and then ran out of the deadline gets it too. It’s checked per key and per server (each server counts separately), after the per-key rate limit and before the body is read. The cap of 4 is a server setting: you can’t change it for your account. It’s separate from rate_limited (about 100 requests a second, each server counts separately): you can hit this one well under 100 a second by sending many requests at the same moment. Example:

HTTP/1.1 429 Too Many Requests
X-Request-Id: req_TGZh9gTigKohFxCU
Retry-After: 1
{"error":{"code":"concurrency_limited","message":"This agent key already has 4 decide requests in flight (per-key concurrency limit, each server counts separately) and no slot freed up in time. Send fewer at once and retry after 1s. No decision was made and nothing was counted.","limit_scope":"per_key_concurrency","request_id":"req_TGZh9gTigKohFxCU"}}

Retry-After is always 1. The body has "limit_scope": "per_key_concurrency".

Tip: agents that make parallel tool calls can hit this. Many agent frameworks run several tool calls at once, and each one checks its plan. One agent fanning out 10 or 20 tool calls on the same agent key at the same moment can go over the limit. Cap how many tool calls run at once per agent key (a small queue or semaphore in your service), or retry the refused ones after 1 second with the same idempotency key.

SDK: handled like rate_limited: a limit, not an outage. The SDK blocks the call in every failMode and never fails open on it. result.rateLimited is true, result.reason is 'concurrency_limited', result.error.limitScope is 'per_key_concurrency', result.retryAfterSec is 1, onRateLimited(result) runs, and your caller gets the SDK’s 429 rate_limited with Retry-After: 1. The warning (at most once a minute) says: [ordainer] concurrency limited: this agent key has too many plan checks in flight at once (Ordainer's per-key concurrency limit, 429 concurrency_limited). This is a rate limit, not an outage. Calls are being blocked (429 rate_limited) in any failMode. Send fewer calls in parallel: retry after 1s. X-Request-Id=… tool=….

How to fix it: send fewer requests at once on that key (see the tip), then retry after 1 second with the same idempotency key. If one key legitimately needs more at once, contact support.

Retry? Yes, after Retry-After (1 second), with the same idempotency key.

Shadow mode never blocks a call because of a plan or test limit. After your test keys reach their monthly limit, each shadow call still counts and is logged as a would-deny with the reason test_limit_reached, and the answer still tells your tool to run. To protect the service, shadow calls have a safety ceiling of 10 times your test limit. Past it, they return HTTP 429 with safety_limit_reached until your test usage resets at the start of the next month (UTC), and those calls aren’t counted or recorded. That’s a safety limit, not a deny. Our SDK (version 1.2.0 and later) lets the tool run automatically. If you call our API directly, we recommend letting the tool run and logging the error. If you reach it, email support@ordainer.ai.

An error, not a decision: there’s no decision body and no audit row, so Console → Decisions won’t find its request ID. error.limit_scope is "org_safety", error.resets_at is the start of next month (UTC), and Retry-After counts the seconds to it. Enforce calls are unaffected (past the test limit they get the test_limit_reached deny), and so are live keys.

SDK (1.2.0): a safety limit, not a deny: the call runs in every failMode, 'closed' included. result.allowed: true, result.safetyLimited: true, result.failOpen: false, result.rejected: false, result.error.limitScope: 'org_safety', result.resetsAt and result.retryAfterSec; onSafetyLimit(result) runs on every such call, and a [ordainer] safety limit reached: … warning is logged at most once per warningIntervalMs. SDK 1.1.0 blocked it like other 4xx answers.

Example (an organization with a monthly test limit of 3, so a ceiling of 30):

HTTP/1.1 429 Too Many Requests
X-Request-Id: req_exampleSafety01
Retry-After: 2160000
{"error":{"code":"safety_limit_reached","message":"This account passed our safety limit for shadow calls (10 times its monthly test limit), so we've stopped checking and recording them until your test usage resets at the start of next month (UTC). This is a safety limit, not a deny: our SDK lets your calls run. Email support@ordainer.ai and we'll help.","limit_scope":"org_safety","resets_at":"2026-11-01T00:00:00.000Z","request_id":"req_exampleSafety01"}}

What it means: your address (IP; for IPv6, its /64 block) sent more than 30 requests a minute with a missing or unknown key (a wrongly shaped key included), and this request’s key is missing or unknown too. It replaces the 401 you’d otherwise get. A request with a valid key is not refused by this limit, even from the same address: one service with a bad key can’t block your other services (see the rare exception below). Revoked keys, old sk_ keys (old_key_format) and successful calls don’t use up the budget. The failed-key allowance comes back at one every 2 seconds, and like the per-key limit it’s counted on each of our servers separately. No decision was made and nothing was counted. The body has "limit_scope": "per_address". Captured on a local copy of main with the limit lowered to 3 a minute (hence the longer Retry-After; with the default 30 a minute it’s about 2 seconds):

HTTP/1.1 429 Too Many Requests
X-Request-Id: req__wE0vFQCtAcug4XU
Retry-After: 20
{"error":{"code":"too_many_failed_auth","message":"Too many requests with a missing, unknown or malformed agent key from this address (per-address limit). Fix the key; valid keys are never refused by this limit. No decision was made and nothing was counted.","limit_scope":"per_address","request_id":"req__wE0vFQCtAcug4XU"}}

The rare exception (a possible wrong deny): when your address is over its budget and a well-formed key isn’t one the server already knows (for example, one created moments ago), the server checks whether the key exists before refusing it. If that check can’t be made right then (the server is overloaded or the check timed out), the request gets this 429 with a different message, so a valid key isn’t refused silently:

{"error":{"code":"too_many_failed_auth","message":"Too many failed requests from this address, and the server could not check this key just now (per-address limit). If the key is valid, retry after the Retry-After seconds; it will work. No decision was made and nothing was counted. Quote the request_id to support.","limit_scope":"per_address","request_id":"req_…"}}

If you see this message with a key you know is valid, retry after Retry-After and send the request_id to support: the server records each of these cases by request ID, so support can confirm it.

How to fix it: fix the key: find the service sending a missing, mistyped, cut-off or deleted key (it gets this code or 401 missing_key / unknown_key), and give it a valid key. Waiting Retry-After alone won’t help while it keeps sending the bad key, and a valid key would have gone through anyway.

SDK: a bad key, not a rate limit: blocked in every failMode like other 4xx (500 plan_check_rejected, result.rejected: true, result.rateLimited: false, result.error.limitScope: 'per_address'; onRateLimited is not called). Its own warning: [ordainer] rejected (call blocked in any failMode): too many requests with a missing or unrecognized agent key from this server's address (Ordainer's per-address limit, 429 too_many_failed_auth). Check the agent key (apiKey / ORDAINER_KEY); calls with a valid key are not affected by this limit. X-Request-Id=… tool=….

Retry? Only after fixing the key.

What it means: a bug or a database failure on our side.

{"error":{"code":"internal_error","message":"Something went wrong. Quote the request_id to support.","request_id":"req_…"}}

How to fix it: nothing to fix on your side. If it keeps happening, send the request_id to support.

SDK: follows failMode (a 500 means Ordainer couldn’t answer; the SDK doesn’t retry a 500, it applies failMode straight away); the result code is internal_error, because the SDK passes the API’s code through. (server_error is only for a 5xx with no error body.)

Retry? Yes, with the same idempotency key, so the call can’t be counted twice. Without a key, a retry is a new decision.

What it means: our database didn’t answer in time: a query on the decision path took longer than 2 seconds and was cancelled. Nothing was counted or recorded, so a retry can’t double-count. The answer has Retry-After: 1. Example (time limit lowered for the capture):

HTTP/1.1 503 Service Unavailable
X-Request-Id: req_NGx9FX2lxCXAgM7k
Retry-After: 1
{"error":{"code":"db_timeout","message":"The database did not answer in time. Retry with the same idempotency key.","request_id":"req_NGx9FX2lxCXAgM7k"}}

How to fix it: nothing to fix on your side. Retry after a second with the same idempotency key. If it keeps happening, send support the request_id.

SDK: it retries once by default with the same key, then follows failMode (except a sticky deny: within 60 seconds of a quota_exceeded deny for the same key and tool, the call is blocked with 429 plan_limit_reached instead of failing open). Under failMode: 'closed' your caller gets 503 ordainer_unavailable with Retry-After: 5; under 'open' the call runs. The result code is db_timeout.

Retry? Yes, after Retry-After (1 second), with the same idempotency key.

What it means: the decision didn’t finish within our server’s deadline (250 ms by default, under the SDK’s 300 ms timeout). Waiting for a database connection, a query or the transaction ran past it, so the decision was rolled back: nothing was counted or recorded. The answer has Retry-After: 1.

HTTP/1.1 503 Service Unavailable
X-Request-Id: req_…
Retry-After: 1
{"error":{"code":"decide_timeout","message":"The decision did not finish within the server's 250 ms deadline, so it was rolled back: nothing was counted or recorded. Retry (with the same idempotency key, if you sent one).","request_id":"req_…"}}

How to fix it: nothing to fix on your side. Retry after a second with the same idempotency key; because nothing was recorded, the retry is decided fresh and counted once. If it keeps happening, send support the request_id.

SDK: the SDK retries a 503 once by default (retries, same idempotency key), then follows failMode: under 'open' the call runs (unless a sticky deny applies); under 'closed' your caller gets 503 ordainer_unavailable. The result code is decide_timeout.

Retry? Yes, after Retry-After (1 second), with the same idempotency key.

What it means: our database couldn’t be reached or used, so no decision was returned. Causes: the connection was refused, failed to authenticate, or dropped; no database connection was free in time; or the database cancelled this decision to resolve a lock conflict (a deadlock or serialization failure), in which case it was rolled back and nothing was counted or recorded. The answer has Retry-After: 1.

HTTP/1.1 503 Service Unavailable
X-Request-Id: req_…
Retry-After: 1
{"error":{"code":"db_unavailable","message":"The database is unavailable right now, so no decision was returned. Retry with the same idempotency key, if you sent one (a decision that did commit is then replayed, not counted twice).","request_id":"req_…"}}

For a lock conflict the message is: The database had to cancel this decision to resolve a lock conflict, so nothing was counted or recorded. Retry with the same idempotency key, if you sent one.

How to fix it: nothing to fix on your side. Retry after a second with the same idempotency key: if a decision did commit before the connection dropped, the retry gets it back instead of being counted twice. If it keeps happening, send support the request_id.

SDK: the SDK retries a 503 once by default (retries, same idempotency key), then follows failMode: under 'open' the call runs (unless a sticky deny applies); under 'closed' your caller gets 503 ordainer_unavailable. The result code is db_unavailable.

Retry? Yes, after Retry-After (1 second), with the same idempotency key.

What it means: the server has no database configured (DATABASE_URL is not configured.). This only happens on a self-hosted or local server, never on Ordainer’s hosted API.

How to fix it: set DATABASE_URL on that server and restart it.

What it means: the public demo endpoint (POST /demo/decide) has no demo data because the database migrations haven’t been run. Like db_not_configured, this is for self-hosted or local servers.

How to fix it: run the database migrations on that server.


The landing page’s “Produce a deny” demo can return rate_limited (the hourly per-address limit), and invalid_request, invalid_json, payload_too_large, internal_error or db_timeout, described above. To keep going past the demo’s limit, sign up for your own test keys.


When something you do in the console is refused, you get an HTML page, not JSON: Something went wrong (or Not found), the message, and a Reference with the request ID. Quote the reference to support. The code isn’t shown on the page; it’s listed here so each one has an entry. These never come from /v1/decide.

The Terms box is on the page the emailed sign-in link opens, shown only at sign-up and after a new version of the legal documents (the /login form itself has no box). Clicking Continue without ticking it gets the page back (HTTP 400, no account and no session) with To continue, tick the box to agree to the Terms of Service and Data Processing Addendum. Tick it and click Continue again; the link still works. If the versions changed while the page was open, the message is Our legal documents changed while this page was open. Please review them and tick the box again.

Besides subscription_required (402, creating a live key without a subscription) and the four codes after this table (csrf_failed, conflict, subscription_active, billing_not_configured), these come from editing plans and moving keys (Console → Plans and Keys). Plan edits, mode switches, key moves, and key creation and revocation that succeed are recorded in Console → Change log (view-only in the console; it can’t be exported).

Status Message (as shown) When Fix
400 "max_allows" must be a whole number from 0 to 1,000,000,000. Saving a plan with a cap that’s empty, negative, not whole, or too big. Enter a whole number in that range.
400 "tool" must be 1-200 characters. Saving a plan with a tool name over 200 characters. A plan covers one tool name or * (every tool), never a list. Spaces around the name are trimmed, and an empty tool means *. Shorten the name.
400 A revoked key cannot be moved. Moving a revoked key. Create a new key on the plan you want.
400 Plan "<name>" counts per <period>, this key's plan per <period>. Move between plans with the same period. Moving to a plan with a different period. Console plans are all monthly, so you’d only see this with plans made outside the console. Pick a plan with the same period.
400 Choose a valid plan. The move form was sent without a valid plan. Pick a plan from the list.
404 No such plan. / No such key. The plan or key doesn’t exist in your organization. Reload the page and try again.
400 see cross_customer_move Moving or creating a key on another customer’s plan. See below.

cross_customer_move · 400 · console only

Section titled “cross_customer_move · 400 · console only”

What it means: the plan you picked belongs to a different customer. A plan is either shared by all your customers or belongs to one customer (chosen when the plan is created), and a key can only use a shared plan or one of its own customer’s plans. The console’s move to plan list only offers allowed plans, so you’ll normally see this only from a stale page or a hand-made request.

Moving a key:

Something went wrong
Plan "acme-only" belongs to Acme, not Example Co (sandbox). A key can only move to a shared plan or one of its own customer's plans; to serve another customer, create a new key.
Reference: req_MTlXdITbrJrW1C4m

Creating a key: That plan belongs to another customer. Choose a shared plan or one of this customer's plans.

How to fix it: pick a shared plan or one of this customer’s plans. To serve another customer, create a new key for that customer. A plan’s owner can’t be changed after it’s created.

Retry? No, not with the same plan.

csrf_failed · 403 · console and sign-in only

Section titled “csrf_failed · 403 · console and sign-in only”

What it means: a console or sign-in form was sent without its security token, or with an old one. Usually the page was open too long, was opened in another tab after you logged out and in again, or a browser extension stripped the form field.

On a console page: Missing or invalid CSRF token. Reload the page and try again. On the sign-in pages: This form expired. Go back, reload the page and try again. (quoted from the code)

How to fix it: reload the page and submit again. If it keeps happening, try a private window without extensions and send support the Reference.

Retry? Yes, after reloading the page.

What it means: you added a customer with an external id that another of your customers already has. External ids are unique within your organization.

Message: A customer with that external id already exists.

How to fix it: use the existing customer (Console → Customers), or give the new one a different external id. You can also leave the external id empty.

Retry? After changing the external id.

subscription_active · 409 · console only

Section titled “subscription_active · 409 · console only”

What it means: you tried to delete your account (Billing → Account → Delete account) while the subscription is still active or past due. The button is greyed out in that state, with the note “Cancel your subscription in “Manage billing” first. You can delete the account once the subscription has ended.”

Message: This account still has an active or past-due subscription. Cancel it in Billing > Manage billing first; you can delete the account once it has ended.

How to fix it: cancel in Manage billing and wait for the paid period to end, then delete. Typing a name that doesn’t match the account’s gives 400 invalid_request (Type the account name exactly ("…") to confirm the deletion.) instead.

The same code also blocks deletion while an unpaid subscription is inside its payment window (until day 14 after the first failed payment) (“This account’s subscription is unpaid and inside its payment window (until ). …”), while its cancel with our payment processor is going through (“The subscription is being canceled by our payment processor. Try again in a few minutes.”), after that cancel failed (“Canceling the subscription with our payment processor didn’t go through, and our team has been alerted to finish it. …”), and while our payment processor has it unpaid but not canceled yet (“This account’s subscription is unpaid with our payment processor and not canceled yet. …”). Each state has its own message, and each tells you what to do next. If you’re unsure, email support@ordainer.ai.

Retry? After the subscription has ended.

billing_not_configured · 503 · console only

Section titled “billing_not_configured · 503 · console only”

What it means: the Billing page’s Subscribe or Manage billing button couldn’t reach our payment processor because billing isn’t set up on that server. On the hosted service this should never happen; on a self-hosted or local server it means billing isn’t configured. It never affects /v1/decide or your keys.

Message: Billing is not configured yet.

How to fix it: hosted service: contact support with the Reference. Self-hosted: configure billing (payment-processor keys, price and webhook secret).

Retry? Not until it’s fixed.

live_keys_unavailable · 403 · console only

Section titled “live_keys_unavailable · 403 · console only”

What it means: you tried to create or reissue a live key (ord_live_) in Console → Keys, and live keys aren’t available yet. The console shows this as a page, not JSON. It never affects /v1/decide or your test keys.

Message: Live keys are not available yet. Live keys open when paid plans launch. Your test keys cover everything in your beta.

How to fix it: keep using test keys (ord_test_).

Retry? No.


These are reason values on a 200 answer with "decision": "deny". They are not errors, but they are listed here because they are what you’ll be asked about. Full table: Decide → Reasons.

The key’s plan allowance for this period is used up. Not counted. Don’t run the tool in enforce; in shadow, run it (it’s recorded as “would deny”). The answer won’t change before resets_at unless you raise the plan’s allowance or move the key to a plan with a higher allowance in the console (the key’s count this period carries over, so the new allowance applies to it right away). Lowering an allowance below what a key already used also produces this deny, with remaining: 0. The Node SDK turns it into plan_limit_reached.

The key’s plan covers a different tool. A plan covers one tool, or every tool (*), never a list. Spaces around tool are trimmed, then names are compared exactly and case-sensitively, so " research.run " matches research.run but Research.run doesn’t. Waiting won’t help; send the right tool name, change the plan’s tool (Console → Plans), or move the key to a plan that covers the tool (Console → Keys). The Node SDK turns it into plan_denied.

test_limit_reached · 200 deny (API) · 429 (SDK)

Section titled “test_limit_reached · 200 deny (API) · 429 (SDK)”

What it means: test keys only. Your organization used its free test-key decisions for this UTC month (1,000 by default, or a limit we’ve set for your organization; all test keys together; only allowed decisions count, including console “Call tool” clicks, and denies are free; retries with the same idempotency key don’t). It’s our sandbox limit, not your customer’s plan. Live keys are never affected.

From the API it’s a deny, not an error. limit, remaining and resets_at describe the test-key limit, not the key’s plan: limit is your organization’s monthly limit (1,000 by default), remaining is 0, and resets_at is the 1st of next month. The plan’s counter isn’t touched. In enforce, mode is enforce, the call isn’t counted, and there’s an extra message. Captured with the limit lowered to 3:

{"decision":"deny","reason":"test_limit_reached","remaining":0,"limit":3,"resets_at":"2026-11-01T00:00:00.000Z","mode":"enforce","request_id":"req_QfwkC-xTBgdwQ3FB","message":"This organization has used all of its free test-key decisions for this month (UTC). Test keys answer deny test_limit_reached until 2026-11-01T00:00:00.000Z. Live keys are not affected. The sandbox reset does not reset this limit."}

In shadow (the plan’s mode or the request’s), the answer has mode: "shadow" and no message, like any other would-deny: run the tool. It doesn’t use up test decisions, but it does count toward a safety ceiling of 10 times the limit, then safety_limit_reached. See Shadow mode at the test limit.

In enforce, the Node SDK blocks it regardless of failMode, and answers your caller 429 with Retry-After (seconds until resets_at). In shadow, SDK 1.2.0 runs the call (result.allowed and result.wouldDeny are true); 1.1.0 blocked it. The enforce body has reason and resets_at but no remaining/limit:

HTTP/1.1 429 Too Many Requests
X-Request-Id: req_fYCAwHRwx-pqRZvS
Retry-After: 2616930
{"error":{"code":"test_limit_reached","message":"This call wasn't run. This service's test calls for the month are used up. They reset on November 1 (UTC). Nothing is wrong on your side, and trying again won't help until they reset. If you need it sooner, contact the service's team. (test_limit_reached)","request_id":"req_fYCAwHRwx-pqRZvS","reason":"test_limit_reached","resets_at":"2026-11-01T00:00:00.000Z"}}

result.limit and result.remaining on the SDK’s result are the API’s values (the test-key limit and 0).

The SDK also sets result.testLimitReached: true, logs its own warning at most once a minute, and calls onTestLimit(result) on every such call.

How to fix it: wait for the 1st (UTC), which is 8 PM ET (7 PM ET in winter) on the last day of the month. Reset sandbox does not clear it. Stop calling the real API from CI: mock the call in unit tests and keep a few real smoke tests. The console Overview shows “N of 1,000 test decisions this month” (or your organization’s own limit), and on the Decisions page the row reads 0/1,000 (your monthly test limit, not the plan allowance) (with your own limit in place of 1,000 if we’ve set one). If your organization needs more for a real pilot, contact sales: only we can change the limit. That change is kept in our records and is not listed in your Change log.

Retry? Not before resets_at.


When the Node SDK’s middleware() or the Ruby gem’s Rails guard blocks a call, it answers your caller (the agent) with one of these. The Node SDK (1.2.4, on npm) and the Ruby gem (0.1.1, on RubyGems) send the same status, code and message. They use the same conventions as the API: an X-Request-Id header (the Rails guard names it X-Ordainer-Request-Id, since Rails uses X-Request-Id for its own ID) and {"error":{"code","message","request_id", …}}. Node’s wrap() throws OrdainerDeniedError, and Ruby’s decide! raises an Ordainer::BlockedError subclass, each with the same status, headers and body. The middleware can answer 429, 402, 403, 500 or 503. Captured from the SDK against a local server (f1baae0 for plan_limit_reached, plan_denied and ordainer_unavailable, which didn’t change; 98d5d41 for test_limit_reached; 7e04c9d for rate_limited and plan_check_rejected).

What it means: the API said deny / quota_exceeded in enforce mode. Your handler did not run.

It’s also sent for a sticky deny: within stickyDenyMs (default 60 seconds) of a quota_exceeded deny for the same agent key and tool, a call that would otherwise fail open (timeout, network error, 5xx, unreadable answer) gets this answer instead of running. Then result.stickyDeny is true and result.error says what failed. In shadow mode the call still runs. Per process, in memory: see Sticky deny.

HTTP/1.1 429 Too Many Requests
X-Request-Id: req_HIqElLUzDXuutya_
Retry-After: 2618226
{"error":{"code":"plan_limit_reached","message":"This agent's plan limit is reached (0 of 5 left). It resets at 2026-11-01T00:00:00.000Z. Retrying before then will not succeed. This call was not run.","request_id":"req_HIqElLUzDXuutya_","reason":"quota_exceeded","remaining":0,"limit":5,"resets_at":"2026-11-01T00:00:00.000Z"}}

Retry-After is always the real number of seconds from the call until resets_at, rounded up (at least 1): here about 30 days; an hour before the reset it would be 3600. The message is written so an agent can show it as-is. If your clients treat 429 as “retry soon”, pass denyStatus: 402 to the client, or to one route’s middleware({ tool, denyStatus: 402 }). Only 429 and 402 are accepted: anything else throws a TypeError when the client or the middleware is created. It changes only this response.

Retry? Not before resets_at.

What it means: the API said deny for a reason other than quota_exceeded or test_limit_reached (today: tool_not_in_plan) in enforce mode. No Retry-After: waiting won’t help.

HTTP/1.1 403 Forbidden
X-Request-Id: req_fzzhewJb0vlD-opH
{"error":{"code":"plan_denied","message":"This call wasn't run because your plan doesn't include \"research.run\". If you need it, ask whoever manages your plan. (tool_not_in_plan)","request_id":"req_fzzhewJb0vlD-opH","reason":"tool_not_in_plan"}}

Retry? No.

What it means: the API answered 429 rate_limited (Each agent key gets about 100 requests a second, each server counts separately. limit_scope is "per_key".), so the SDK blocked the call. It does this in every failMode. The SDK logs a [ordainer] rate limited: this agent key is over Ordainer's per-key request limit (429 rate_limited). This is a rate limit, not an outage. … warning at most once a minute, sets result.rateLimited: true, result.error.limitScope: 'per_key' and result.retryAfterSec, and calls onRateLimited(result) on every such call. Captured from the SDK’s denyResponse() at 7e04c9d against a local server with the limit lowered to 1 a second:

HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-Request-Id: req_8gxH8vWssolU5LPO
{"error":{"code":"rate_limited","message":"Too many plan checks for this agent right now (Ordainer's per-key rate limit). Retry after 1s. This call was not run.","request_id":"req_8gxH8vWssolU5LPO","retry_after":1}}

The SDK sends the same response for 429 concurrency_limited (too many requests in flight at once on the key), with Retry-After: 1; result.reason and the warning say which limit it was. Retry-After and retry_after are the server’s Retry-After. This response has no limit_scope: only the per-key limits get here. The per-address 429 too_many_failed_auth is a bad key, so the SDK answers it with 500 plan_check_rejected instead, and its warning never says “per-key”.

Retry? Yes, after Retry-After seconds.

plan_check_rejected · 403 or 500 (SDK only)

Section titled “plan_check_rejected · 403 or 500 (SDK only)”

What it means: the SDK blocked the call because the plan check refused it for a reason a retry can’t fix. This code never comes from the API; only the SDK’s middleware() and wrap() send it, to your caller. There is no Retry-After and the message says retrying won’t help, so agents don’t hammer it. Two statuses (since SDK 0.2.0, 0791c39; before that these were 503 ordainer_unavailable with no Retry-After):

  • 403: billing. Your Ordainer account has no paid subscription for a live key (subscription_required, any setting), or your subscription has lapsed (subscription_inactive) and you set billingLapse: 'closed'. Your caller gets a neutral message that doesn’t mention billing or Ordainer. From the SDK, after 403 subscription_required:
HTTP/1.1 403 Forbidden
X-Request-Id: req_-gMcsXmLJSEhVsXb
{"error":{"code":"plan_check_rejected","message":"This action isn't available right now. This is a setup issue on the service's side; retrying won't help.","request_id":"req_-gMcsXmLJSEhVsXb"}}
  • 500: setup. Any other API error (any other 4xx: missing_key, unknown_key, old_key_format, revoked_key, too_many_failed_auth, invalid_request, idempotency_conflict, …), or no key or tool configured in your code. Blocked in every failMode; result.rejected is true. It’s a 500 on purpose: the problem is in your service (a bad or revoked key, or a configuration problem), not in the agent’s request, and automatic retries that do happen fail fast. From the SDK, after 401 unknown_key:
HTTP/1.1 500 Internal Server Error
X-Request-Id: req_-gsSOSegpvgFfUYB
{"error":{"code":"plan_check_rejected","message":"The plan check refused this call because of a setup problem on the service's side, so it was not run. This is not an outage: retrying will not help until the service operator fixes its setup.","request_id":"req_-gsSOSegpvgFfUYB"}}

X-Request-Id and request_id carry the API error’s request id (null, and no header, when the SDK never called the API, such as with no key). The API’s own code is on result.error.code and in the SDK’s warning.

How to fix it: the fix is on your side (the service that runs the SDK), not the agent’s. For a 500: check the agent key (apiKey / ORDAINER_KEY: missing, mistyped, revoked) and your setup (base URL, tool name, how you build idempotency keys); the SDK’s [ordainer] rejected … warning names the API code and request id. For a 403: subscribe or resubscribe in Console → Billing.

Retry? No. Retrying won’t help until the key, the setup or the billing is fixed.

What it means: Ordainer couldn’t answer (timeout, network error, 5xx such as decide_timeout, db_unavailable or db_timeout, unreadable answer) and you set failMode: 'closed', so the SDK blocked the call. With the default failMode: 'open' the call runs instead. result.failClosed is true. Since SDK 0.2.0 this code means a real outage only; a refused call is plan_check_rejected.

HTTP/1.1 503 Service Unavailable
Retry-After: 5
{"error":{"code":"ordainer_unavailable","message":"This call wasn't run. The service couldn't finish a safety check it does before each call, so it stopped instead of going ahead without it. Nothing is wrong on your side. Please try again in a minute. (ordainer_unavailable)","request_id":null}}

See Fail open vs fail closed. request_id is null and there’s no X-Request-Id when the API was never reached; after a 5xx with an error body, both carry that error’s request id.

Retry? Yes, after 5 seconds.


decide() never throws. When it can’t get a decision, the result has error: { code, message, status, limitScope } (limitScope only on a 429: 'per_key', 'per_key_concurrency', 'per_address' or 'org_safety', from the API’s limit_scope; it’s a separate field from result.limit, the plan’s allowance). After a timeout, network error, 5xx or unreadable answer, allowed follows failMode, and failOpen or failClosed is true. After 403 subscription_inactive (your lapsed subscription), allowed follows billingLapse, not failMode (SDK 1.1.0 and later): with 'open' (the default) allowed is true with failOpen and failClosed both false; with 'closed' allowed is false and rejected is true. Either way billingInactive is true and billingLapse says which setting applied. After 429 safety_limit_reached, allowed is true in every failMode (SDK 1.2.0), with safetyLimited true and failOpen false. After any other API error (any 4xx) or a missing key or tool, allowed is false in every failMode, and rejected is true (failClosed stays false; at most one of the three flags is true). The SDK logs a [ordainer] fail-open (call allowed), fail-closed (call blocked) or rejected (call blocked in any failMode) line at most once a minute per mode and code (warningIntervalMs, default 60000), with “(N more since the last warning)”; onDecision still sees every result. Besides the API codes above (passed through as-is when the answer has an error body, e.g. unknown_key or internal_error), and the client-side missing_key and invalid_request, these come from the SDK itself:

No answer within timeoutMs (300 ms by default). Example message: Ordainer did not answer within 300ms. Retry? Yes; consider a longer timeoutMs if it’s frequent.

The request never got an answer: DNS, connection refused, TLS. Example message: fetch failed. When the baseUrl host doesn’t resolve, error.dnsCode is 'ENOTFOUND' or 'EAI_AGAIN' and SDK 1.2.0 logs one error-level line the first time, naming the host and saying to set ORDAINER_BASE_URL. Retry? Yes. Check baseUrl.

Only for a 5xx answer with no error body, for example an HTML error page from a proxy or load balancer in front of the API. A normal 500 from the API has the error body, so it shows up as internal_error, not server_error. Retry? Yes, with the same idempotency key.

A non-5xx answer that isn’t a decision and has no error body, or a 200 whose body isn’t a decision. Usually a wrong baseUrl pointing at something else. A 4xx without an error body is still blocked in every failMode; anything else follows failMode. Retry? Maybe: fix baseUrl first.