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 ontitleordetail: they are written for people, can change wording, and are in English whatever language you ask for (detailmay 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. typeis 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.requestIdis what to quote when you ask Anis about a call. It is also in theX-Request-Idheader 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:
- The address. You call the address Anis gave you, and the host you sign as
@authorityis that address, in lower case. A proxy that changes the host breaks every call. - The key id and the key file.
keyidis the key id you received at enrolment, and you sign with the private half of that same key. - The key’s state. Read the enrolment status: only an
activekey signs. A revoked or expired key, or an old key once its replacement’s overlap has ended, is refused on every call. - Your clock. Your server clock is synchronised (NTP),
createdis the time you sign, and the signature lives 60 seconds or less — see Clock. - The signature. It is the 64-byte form, never DER, and
algisecdsa-p256-sha256. - 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:
- Wait 1 second after the first refusal.
- Double the wait after each further refusal in a row: 2, 4, 8, 16 seconds.
- Never wait more than 30 seconds.
- Add a random jitter to every wait, so that your workers do not all send again at the same moment.
- 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.