SALAF EMSDevelopers
salafems.com

Rate limits

Per-key buckets, headers, and handling 429.

Requests are counted per API key, in a fixed 60-second window, across two separate buckets.

BucketCountsDefault
ReadsGET, HEAD, OPTIONS120 per minute
WritesEverything else30 per minute

The two buckets are counted separately, so a bulk catalogue import cannot starve the reads a dashboard is making with the same key — and neither can exhaust the other's allowance. The write bucket is the smaller one on purpose: writes cost far more work per request, and 30 a minute is well above what a correctly batched integration needs.

Headers on every response

You never have to guess where you stand — the numbers are on every response, not just the ones that fail.

HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1786521600
HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window, for this bucket.
X-RateLimit-RemainingHow many are left. Floors at 0.
X-RateLimit-ResetUnix timestamp, in seconds, when the window resets.

When you exceed it

HTTP/1.1 429 Too Many Requests
Retry-After: 24
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1786521600
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded: 120 read requests per minute. Retry in 24s.",
    "request_id": "4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59"
  }
}

Retry-After is exact. It is the number of seconds left in your window, not a suggestion and not a rough backoff hint. Sleeping for less means spending another request to be told the same thing; sleeping for more just wastes your own time.

Paste a key and every snippet on this page switches from the placeholder to your key — and the Try it panel under each endpoint is ready to send. It is stored in this browser only and goes nowhere except, if you press Send, straight to the API host you pick there.

# --retry 5 makes curl honour Retry-After on a 429 by itself.
curl -sS --retry 5 --retry-all-errors \
  "https://api.salafems.com/ext/v1/orders?limit=100" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -D headers.txt

# The headers on EVERY response tell you how much room is left:
#   X-RateLimit-Limit: 120
#   X-RateLimit-Remaining: 3
#   X-RateLimit-Reset: 1786521600
grep -i '^x-ratelimit' headers.txt

How the window works

The window is fixed, not sliding: the minute is divided into fixed blocks and each one starts your count at zero. One consequence is worth knowing — you can briefly do up to twice the limit across a boundary, by spending your full allowance at the end of one window and again at the start of the next.

That forgiveness is intentional. A sliding window would be stricter and would cost more to run, and nobody has yet demonstrated that the edge behaviour matters here. If that changes, tightening it is a change to our side only.

Per key, not per company

Each key gets its own buckets. A company key counts as one key, exactly like a store key.

This has a practical implication worth planning around: splitting one integration across several keys multiplies its throughput. That is a legitimate way to run a nightly bulk sync and a real-time listener side by side without one starving the other — and separate keys also mean you can revoke one without touching the other.

There is also a per-IP ceiling in front of everything as a denial-of-service backstop. It is set high enough that ordinary integration traffic never approaches it, including several tenants sharing one office NAT.

Plan limits

The defaults above are floors, not ceilings. Both limits are plan features (api_rate_limit_reads and api_rate_limit_writes), so a higher tier raises them with no change on your side — your key simply starts reporting a larger X-RateLimit-Limit.

Read the limit from the header rather than hard-coding 120. Your plan may already be higher than the default, and it can change without a release on either side.

Staying under the limit

  • Ask for bigger pages. limit=100 fetches the same 1,000 rows in 10 requests instead of 40. The rate limit counts requests, not rows.
  • Filter with updated_after. A sync that reads only what changed makes a fraction of the calls of one that re-reads everything.
  • Do not poll tightly. Once a minute is plenty for almost every integration; once every few minutes is plenty for most.
  • Watch X-RateLimit-Remaining and slow down before you hit zero. Backing off at 10 remaining is cheaper than a 429 and a retry.
  • Do not expand what you will not read. expand=items on a page you only need totals from is work for both of us.