Errors and limits

What a refusal carries

Every refusal is a JSON problem — signed by Anis on the calls whose answers are signed (see Verifying answers):

{
  "type": "https://developers.anis.ly/errors/price-changed",
  "title": "Price changed",
  "status": 409,
  "code": "price_changed",
  "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F"
}
  • Branch on code. Never on title or detail: they are written for people, can change wording, and are in English whatever language you ask for (detail may be absent). To show your users a message in their language, use the code’s page in Error codes — every page exists in Arabic too.
  • type is the address of the code’s page — every code has one in Error codes, saying what it means, whether an order was bought and what to do.
  • requestId is what to quote when you ask Anis about a call. It is also in the X-Request-Id header of every answer.

Two things a refusal may carry

  • Retry-After — when Anis knows how long to wait, in seconds. Honour it.
  • Idempotency-Replayed: true — on an order: the recorded answer of an order that is already closed.

Refusals that hide which rule failed

Some refusals give the same answer for different causes, on purpose, so that a refusal never tells anyone which thing to work around:

Code Covers
invalid_credentials an unknown key, a key that is not active, a revoked key, a wrong signature, an expired signature, a call to an address other than the one Anis gave you
malformed_signed_request a signature header that is missing or cannot be read, signed parts that are not the ones the route needs or not in its order, a Content-Digest that cannot be read or does not describe the body that was sent
insufficient_scope a missing permission, or a call from outside your allowed networks
wallet_not_granted, resource_not_found something that does not exist, or that is not yours
validation_failed A request breaks a rule. Direct recharge recipient validation can include field errors in extensions.fields; do not rely on field details for other refusals.

The 401 checklist

When your calls are refused as invalid_credentials, check these, in this order:

  1. The address. You call the address Anis gave you, and the host you sign as @authority is that address, in lower case. A proxy that changes the host breaks every call.
  2. The key id and the key file. keyid is the key id you received at enrolment, and you sign with the private half of that same key.
  3. The key’s state. Read the enrolment status: only an active key signs. A revoked or expired key, or an old key once its replacement’s overlap has ended, is refused on every call.
  4. Your clock. Your server clock is synchronised (NTP), created is the time you sign, and the signature lives 60 seconds or less — see Clock.
  5. The signature. It is the 64-byte form, never DER, and alg is ecdsa-p256-sha256.
  6. The signature self-check. Run Check your signature and follow what it answers.

If the self-check also fails and nothing changed on your side, your access may be paused: write to support@anis.ly with the requestId of a refused call.

Limits

Anis staff can set limits on your application, for the whole application or per wallet. Each counts one kind of call in a time window:

Limit Counts
All requests every signed call
Orders Creating or resuming a card order, meter charge or direct recharge; all use the existing orders limit.
Reveals both reveals

When a limit is reached the call is refused as rate_limited, with Retry-After when the reset time is known. Anis also protects itself with limits of its own, which give no reset time — the sections below give their numbers and how long to wait. Once the wait is over, send the call again. An order is different: resume it with the same operation id — an earlier copy of it (a call that timed out, or a retry your HTTP client made) may already have gone through, so a rate limit does not prove the order was not placed. See Orders and recovery.

A call counts toward a limit when it arrives, before it is examined. An order refused for its price, your balance or stock has still used one of your orders for that window, so check prices and balance before you order.

The Routes and permissions page shows which limits each call counts toward.

Spending limits are different

owner_limit_exceeded (reserved, not sent today) and daily_limit_exceeded are limits on what the Anis business account may spend, not on how often you call. Waiting does not reset them — do not retry in a loop.

One application spending cap covers card order totals, meter expectedTotal and direct recharge expectedTotal across its wallets. There is no separate meter or direct recharge cap. A new application defaults to 5,000 LYD per day across all its wallets. Windows follow UTC; a one-day window starts at 00:00 UTC (02:00 in Libya). A definitive spending_limit_exceeded refusal is HTTP 429; Retry-After gives the reset delay. Do not retry the refused operation in a loop. After the reset, a new purchase uses a new operation id. An access or transport failure still says nothing about an earlier unknown attempt.

Limits Anis sets for itself

These limits protect every partner. Each Anis edge instance counts them on its own, and Anis runs several, so each number in the first table is a safe envelope: stay within it and that limit never refuses you; go above it and refusals depend on which instance takes each call. Their refusals are rate_limited with no Retry-After.

Limit Safe envelope
Calls from one source address 600 a minute
Orders in flight (sent, answer not received yet) 16 per application
Reveals in flight 8 per application
Every other call in flight 64 per application

A refusal for calls in flight clears as soon as one of your own calls of the same kind ends.

Orders and reveals also count toward one budget shared by every instance: 1200 a minute per application, and 1200 a minute per key. Its refusal is rate_limited with Retry-After.

Every request must also fit these bounds. A request outside them is refused as validation_failed every time — sending it again does not help:

Bound Value
Order body At most 8,192 bytes (8 KB), in UTF-8 — a Content-Type naming any other charset is refused as HTTP 422, validation_failed. Send Content-Length; a larger body is refused as HTTP 422, validation_failed
Other bodies At most 64 KB, sent with Content-Length — chunked transfer encoding is refused for every request
All headers together At most 32 KB
Method, path and query together At most 2048 characters
Cards in one order (quantity) At most 100

Waiting before you send again

When a refusal carries Retry-After, wait that long. When it does not — rate_limited from the limits above, dependency_unavailable, request_timeout or internal_error — wait before each new attempt:

  1. Wait 1 second after the first refusal.
  2. Double the wait after each further refusal in a row: 2, 4, 8, 16 seconds.
  3. Never wait more than 30 seconds.
  4. Add a random jitter to every wait, so that your workers do not all send again at the same moment.
  5. After a success, start again from 1 second.

When an order’s outcome is unknown, every new attempt resumes the same operation id with the same body. A definite spending-cap refusal is different: nothing was placed; after Retry-After, use a new operation id for a new order. See Orders and recovery.

A route Anis has switched off

Anis can switch a route off for a while. Such a route answers a plain 404: no code — and, on a call whose answers are signed, no Anis signature. Your client cannot verify it, so never act on it as an answer. On an order it says nothing about the outcome — keep the operation id and resume it later. If a route keeps answering this way, write to support@anis.ly.