Limits and retries

This page explains how many requests you can make, what happens when you go over the limit, and how to retry write calls without creating duplicates.


Rate limit

Every API token may make 600 requests per minute. The limit is counted per token, so two integrations with their own token do not share a budget.

Requests without a token (for example a failed login attempt) are counted per IP address with a lower limit of 60 requests per minute.

Every response carries the current state of your budget:

  • Name
    X-RateLimit-Limit
    Type
    integer
    Description

    The number of requests allowed per minute.

  • Name
    X-RateLimit-Remaining
    Type
    integer
    Description

    The number of requests you have left in the current window.

Inbound webhook endpoints that Walnut exposes for third parties are not part of this limit.


Over the limit: 429

When you exceed the limit the API answers 429 Too Many Requests and does not process the request. The response tells you when to try again:

  • Name
    Retry-After
    Type
    integer
    Description

    The number of seconds to wait before sending the next request.

  • Name
    X-RateLimit-Limit
    Type
    integer
    Description

    The number of requests allowed per minute.

  • Name
    X-RateLimit-Remaining
    Type
    integer
    Description

    0 while you are limited.

429 response

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
Content-Type: application/json

{
  "message": "Too many requests. Slow down and retry after the number of seconds in the Retry-After header."
}

A request that was answered with 429 has had no effect, so it is always safe to send it again.

How to retry

  • Wait at least Retry-After seconds before the next request, then continue.
  • For 5xx errors and network timeouts, retry with exponential backoff (1s, 2s, 4s, ...) with a bit of random jitter, and give up after a handful of attempts.
  • Do not retry other 4xx errors, they will not succeed without a change to the request.
  • For bulk work, use the batch endpoints (for example POST /products/batch or POST /tags/members/bulk) instead of many single calls.

Idempotency-Key

A timeout on a POST, PUT or PATCH leaves you unsure whether the call was processed. Add an Idempotency-Key header with a unique value (a UUID works well) and you can repeat the exact same call safely:

Request with an idempotency key

curl -X POST https://api.walletapp.co/passes/scan \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer <your_token>" \
  -H "Idempotency-Key: 6f1c2f9e-3b1a-4f0e-9a52-0d7d1f2c8a11" \
  -d '{ "pass_code": "<pass_code>", "brand_id": "<brand_id>" }'

The header is optional and only applies to POST, PUT and PATCH. Without it nothing changes.

  • Name
    Same key, same request
    Type
    replay
    Description

    The first response is returned again with the header Idempotent-Replayed: true. The action is not executed a second time.

  • Name
    Same key, different request
    Type
    422
    Description

    Answered with 422 and "error": "idempotency_key_reuse". Use a new key for a new request.

  • Name
    Same key, first request still running
    Type
    409
    Description

    Answered with 409 and "error": "idempotency_in_progress" plus a Retry-After header. Wait and send the same request again.

  • Name
    Invalid key
    Type
    400
    Description

    The key must be 1 to 255 characters ("error": "idempotency_key_invalid").

Rules

  • A key is scoped to your token, the HTTP method and the path. The same value on another endpoint or with another token is a different key.
  • "Same request" means the same body and query string.
  • Responses are kept for 24 hours. After that the key can be used again.
  • Only successful (2xx) responses are stored. If a request fails with a 4xx or 5xx, the key is released and you can fix the request and retry with the same key.
  • Some endpoints are idempotent in their own right, for example the transaction_reference on POST /passes/scan/storecard. Those keep working as before; the header is an extra safety net on top, and you do not need both.

Reuse with a different body

HTTP/1.1 422 Unprocessable Entity

{
  "message": "This Idempotency-Key was already used with a different request payload.",
  "error": "idempotency_key_reuse"
}