Rate Limits
Every rate limit the Starship API enforces — the rules, what each is keyed on, and how to read the headers
Rate Limits
This page is the single source of truth for Starship's rate limits. Every limit listed here is enforced by the gateway; no other limits exist.
How rules are matched
Rules are evaluated in the order listed below, matching the request path by prefix. The first matching rule wins — a request to POST /api/v1/orders is governed by the orders rule and never also by the general rule.
Each rule is counted against a key, which determines whose budget the request spends:
| Key | Meaning |
|---|---|
| API key | Your X-API-Key. All requests using the same key share one budget, regardless of source IP. |
| Client | Your account. Shared across every credential you hold. |
| IP | The source IP of the request. |
Client API (/api/v1/*)
| Rule | Applies to | Limit | Window | Keyed on |
|---|---|---|---|---|
| Order creation | POST /api/v1/orders | 100 requests | 1 minute | API key |
| Product images | GET /api/v1/products/imagesGET /api/v1/products/{id}/image | 100 requests | 1 minute | Client |
| General | Every other /api/v1/* endpoint | 1,000 requests | 1 hour | API key |
The general rule is the one that will bite you. Product listing, product details, charges, order lookups, wallet reads, transaction history, payouts, beneficiaries and webhook management all draw on the same 1,000-per-hour budget. There is no separate per-endpoint allowance for any of them, and there is no burst allowance layered on top.
Designing around the general limit
The general limit rewards bulk reads over chatty ones:
- Import the catalog in pages, not per product.
GET /api/v1/productsacceptslimitup to 10,000 and returns each product's denominations and your resolved discount in the same payload. A full catalog import costs a handful of requests, not one per product. - Do not call the Charges API per product at import time. It is a quote endpoint, not a prerequisite for ordering — see Pricing and Charges.
POST /api/v1/ordersre-prices server-side at creation and debits the authoritative amount. - Back off when polling order status. 10 seconds is the floor, 60 seconds is preferred for
DELAYEDorders. See Delivery Semantics. - Cache reference data. Countries, currencies and categories change rarely and are served with a 24-hour cache header.
Authentication endpoints
These are keyed on IP, not on your API key, and several apply a block period after the limit is hit — during a block, every request from that IP is rejected until the block expires.
| Rule | Applies to | Limit | Window | Block after breach |
|---|---|---|---|---|
| Login | POST /auth/login | 5 requests | 1 minute | 15 minutes |
| Token refresh | POST /auth/refresh | 20 requests | 1 minute | — |
Automated test suites trip the login limit constantly. Five failed or successful logins from one IP inside a minute earns that IP a 15-minute block. Authenticate once and reuse the token across your suite rather than logging in per test case.
Additional limits apply to portal-only flows (password reset, 2FA enrolment, credential management). They are keyed on the signed-in user and are not reachable from the client API.
Reading the headers
Every response carries your current standing against the matched rule:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1640995200| Header | Meaning |
|---|---|
X-RateLimit-Limit | The matched rule's ceiling |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When you are throttled
A breached limit returns 429 Too Many Requests:
HTTP/1.1 429 Too Many Requests
Retry-After: 1800
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
Content-Type: application/json{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please retry after 30 minutes or contact support to increase your limit.",
"retry_after": 1800
}Branch on the error field, which is the literal string rate_limit_exceeded. The message text is human-readable, varies by rule, and rounds to whole minutes — do not parse it. Use retry_after, which mirrors the Retry-After header and is expressed in seconds.
Honour Retry-After. It is expressed in seconds and tells you exactly when the window reopens; retrying sooner spends nothing but your own capacity. Wrap retries in exponential backoff with jitter so a fleet of workers doesn't resynchronise onto the same reset boundary.
Need a higher limit?
If your traffic profile genuinely needs more headroom, contact your Starship representative with your expected request volume, its shape (steady versus bursty), and which endpoints dominate it. Limits are configured per account and can be reviewed.