---
title: "Rate limits"
description: "Per-store request budgets, rate limit headers, and the 429 response"
---

> API Reference Index
> Fetch the complete API index at: https://docs.firearmcart.com/api/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

The API enforces two rate limit buckets. Both are keyed by **store**, not by token, not by IP, and not by user.

| Bucket | Limit | Applies to |
|--------|-------|------------|
| General | 60 requests per minute | Every `/v1` endpoint |
| Payments | 30 requests per minute | `POST /v1/payments/charge` and `POST /v1/orders/{order_id}/upsell` |

Because the key is the store, **all of a store's tokens share one budget**. Issuing a second token to a second integration does not buy more throughput; it splits the same 60/minute between them.

---

## The two buckets stack

The two payment endpoints sit inside the general group *and* carry the stricter payment throttle, so a single charge consumes one slot in **both** buckets.

In practice:

- 30 charges in a minute exhausts the payment bucket and leaves 30 general requests available for reads.
- 60 reads in a minute exhausts the general bucket and blocks charges too, even though the payment bucket still has room.

Budget for the sum, not for either limit alone.

---

## Windows

Each bucket is a fixed one-minute window. Once the window rolls over, the full allowance is restored at once — there is no leaky-bucket smoothing and no burst credit. Rather than pacing off the clock, read `Retry-After` from the 429 and sleep for exactly that long.

---

## Headers

Every response — successful or not — carries the current bucket state:

| Header | Present on | Meaning |
|--------|-----------|---------|
| `X-RateLimit-Limit` | All responses | Requests allowed in the current window |
| `X-RateLimit-Remaining` | All responses | Requests left in the current window |
| `Retry-After` | `429` only | Seconds until the window resets |
| `X-RateLimit-Reset` | `429` only | Unix timestamp when the window resets |

On the two payment endpoints the headers report whichever bucket is more restrictive at that moment, so a charge response typically shows `X-RateLimit-Limit: 30`. Treat the headers as advisory for the request you just made rather than as a full picture of both buckets.

---

## The 429 response

Exceeding either bucket returns `429 Too Many Requests`:

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 37
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1786425600
```

```json
{
"message": "Too Many Attempts."
}
```

That is the whole body. There is no `error` key, no error code, and no detail about which bucket you hit — the only signal is `X-RateLimit-Limit`, which tells you whether it was the 60 or the 30 bucket.

> **Important:** This JSON body only appears when the request carried `Accept: application/json`. Without that header you get an HTML error page with a `429` status. See [Errors](/api/errors#send-accept-applicationjson).

Some published FirearmCart material describes a `rate_limit_exceeded` error envelope with a `retry_after` field in the body. **No such envelope exists.** The 429 body is exactly the single-key object above, and the retry delay is only ever in the `Retry-After` header.

---

## Handling 429 in your client

```bash
#!/usr/bin/env bash
# Retry once on 429, honouring Retry-After.
response=$(curl -s -D /tmp/h -o /tmp/b -w '%{http_code}' \
  "https://api.firearmcart.com/v1/orders" \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json")

if [ "$response" = "429" ]; then
  delay=$(grep -i '^retry-after:' /tmp/h | tr -d '\r' | awk '{print $2}')
  sleep "${delay:-60}"
  # ... reissue the request
fi
```

Rules that keep integrations healthy:

- **Always honour `Retry-After`.** Retrying sooner just burns another slot and extends the window.
- **Never retry a 429 on a payment endpoint blindly.** `POST /v1/payments/charge` has no idempotency key, so a retry that races the original can double-charge. `POST /v1/orders/{order_id}/upsell` requires an `idempotency_key` and is safe to retry with the *same* key.
- **Page with the largest `per_page` you can use.** `per_page=100` costs one request instead of four. See [Pagination](/api/pagination).
- **Cache the slow-moving reads.** `GET /v1/shipping` and `GET /v1/taxation` return small, rarely-changing payloads and are not paginated; fetch them on a schedule rather than per checkout.

---

## Other buckets

The [Theme Dev MCP server](/api/mcp) at `https://api.firearmcart.com/mcp/themes` uses its own, more generous limit of **240 requests per minute per store**, because agent sessions make many small tool calls. It does not draw on the `/v1` budget, and the `/v1` budget does not draw on it — exhausting one leaves the other untouched. See [MCP rate limits](/api/mcp#rate-limits) for the unauthenticated key and how requests map to tool calls.

Storefront and webhook traffic are limited separately and never consume your API budget.

---

## Related documentation

- [Errors](/api/errors) — every failure shape, per status code
- [Pagination](/api/pagination) — fetching more per request
- [Authentication](/api/authentication) — the store-scoped token model that defines the key
- [Theme Dev MCP server](/api/mcp) — the 240/min bucket in context

Source: https://docs.firearmcart.com/api/rate-limits/index.mdx
