Browse the documentation

Rate limits and idempotency

Rate limits

Six counters, in fixed one-minute windows. A call goes through all of them; the first one full answers 429 API_RATE_LIMITED with Retry-After.

CounterCeilingCounted perApplies to
Address1 200 / mincalling IP addressevery /v1 call, before the key is even read
Reads600 / minkeyGET, HEAD, OPTIONS
Writes60 / minkeyPOST, PATCH, DELETE
Power20 / minkey and servicePOST /services/{code}/power
Reinstall2 / minkey and servicePOST /services/{code}/reinstall
Account1 200 / minaccountevery call, across all keys

Three details that change how a retry loop behaves:

  • A refused call counts. A burst of 400 eats the write budget like successful calls do. Fix the body before retrying.
  • Rotating a key resets its counters. Per-key counters are indexed on the secret's identity, and a rotation creates a new one.
  • Counters are local to each server. Behind several replicas, the ceiling actually reached can go up to twice the nominal one.

Reading the headers

HeaderValue
X-RateLimit-Limitthe ceiling of the tightest of the six counters
X-RateLimit-Remainingwhat is left on that counter
X-RateLimit-Resetepoch timestamp in seconds of the end of the window, not a delay
Retry-Afteron 429 only: seconds to wait, minimum 1

These headers describe the tightest counter at that moment, which is not always the same one from call to call.

Idempotency

Four routes move money and require an Idempotency-Key header:

  • POST /orders
  • POST /orders/{orderNumber}/pay
  • POST /invoices/{code}/pay
  • POST /services/{code}/renew
Idempotency keyDetail
Formatnon-blank string, 128 characters at most. A UUID does
Absent400 IDEMPOTENCY_KEY_REQUIRED
Blank or too long400 IDEMPOTENCY_KEY_INVALID
Scopethe API key. Two keys may use the same string without colliding
Retention24 hours

What makes it "the same request"

The digest covers the method, the concrete path and the body. The same idempotency key on another order number is therefore a different request.

SituationAnswer
Identical replay after a successthe recorded response, with Idempotent-Replayed: true. Nothing is redone
Same key, different request409 IDEMPOTENCY_KEY_REUSED
Same key while the first call is still running409 IDEMPOTENCY_IN_PROGRESS with Retry-After

Refusals replay too

A refusal is a result: it is recorded and replayed. A replayed 402 PAYMENT_DENIED stays a 402, with no second call to the bank.

The exception is a refusal raised before any write and any charge. Those release the key: fix the condition and retry with the same one.

Releases the keyDoes not release it
INSUFFICIENT_BALANCEPAYMENT_DENIED
NO_PAYMENT_METHODPAYMENT_RECORDED_MISMATCH
PAYMENT_METHOD_ID_REQUIREDAPI_KEY_SPENDING_CAP_EXCEEDED
PAYMENT_METHOD_NOT_FOUNDPAYMENT_FAILED
PAYMENT_METHOD_NOT_ACTIVEany 500
PAYMENT_METHOD_DECRYPTION_FAILED
PAYMENT_METHOD_VAULT_ID_MISSING
PAYMENT_IN_PROGRESS
RENEWAL_IN_PROGRESS
RENEWAL_TOO_FREQUENT

API_KEY_SPENDING_CAP_EXCEEDED sits in the right-hand column even though nothing was charged: on POST /orders the authoritative check runs after the order row is written, and releasing the key would let a retry create a second one. Raise the cap, then retry with a new idempotency key.

In practice

  • Generate the idempotency key before the first send, and store it with the task, not in memory.
  • Reuse it as is for every retry of the same task.
  • Change it as soon as the task changes, even by one cent.
    Rate limits and idempotency | FreshPerf