The PrintedSimply API enforces per-plan rate limits to keep the platform fast and fair. Learn the tiers, the response headers, and how to back off gracefully.
Rate limits are applied per account on a sliding window basis. When you exceed your limit, the API responds with 429 Too Many Requests until the window resets. Every response includes headers so you can monitor your usage in real time.
| Plan | Requests / minute | Daily quota | Notes |
|---|---|---|---|
| Free | 60 | 10,000 | Free — default for all new API keys |
| Premium | 300 | 200,000 | Not offered on PrintedSimply — existing accounts only |
| Growth | 120 | 50,000 | Legacy — existing accounts only |
| Scale | 300 | 200,000 | Legacy — existing accounts only |
| Enterprise | 600 | 1,000,000 | Legacy — existing accounts only |
Both the per-minute limit and the daily quota are enforced per account, shared across all of your API keys — creating more keys does not multiply your budget. See pricing for full plan details. Every response states your own limit in X-RateLimit-Limit.
The Free plan is the default for every new account, and these are its complete limits — there is no separate trial, no expiring credit, and nothing on this list changes after a first order.
| Limit | Free plan | Notes |
|---|---|---|
| Requests per minute | 60 | Per account, shared across every key |
| Requests per day | 10,000 | Per account, resets daily |
| Monthly request quota | None | The daily quota is the only ceiling |
| Overage | $0 | Free never bills for API traffic |
| Sandbox orders | Unlimited | $0 each, full lifecycle, real webhooks |
| API keys | 20 | Concurrent keys per account |
There is no monthly request quota and no overage billing on Free. Exceeding the per-minute or daily limit returns 429 with the reset timestamp in the response headers — never a charge.
Sandbox orders are not capped or counted separately. A test key places as many $0 orders as your request quota allows, each running the full time-compressed lifecycle with real signed webhooks.
Every API response the limiter counted includes the following rate-limit headers (a 401 for a missing or unknown key carries none: there is no account whose window they could describe):
| Header | Description |
|---|---|
X-RateLimit-Limit | The maximum number of requests allowed in the current window. |
X-RateLimit-Remaining | The number of requests remaining in the current window. |
X-RateLimit-Reset | Unix timestamp in milliseconds when the current window resets. Wait until this time before retrying — the delay is reset - Date.now(). |
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1710523200000The same window is also sent in the IETF HTTP API working group's RateLimit fields, which a generic HTTP client parses without knowing our units. RateLimit-Policy names each window with its quota (q) and length in seconds (w); RateLimit says what is left (r) and how many seconds until it comes back (t) — a delta, so there is no clock to skew and no unit to guess.
HTTP/1.1 200 OK
RateLimit-Policy: "minute";q=60;w=60
RateLimit: "minute";r=42;t=18A daily_quota_exceeded 429 adds the day, because the day is the window that refused: RateLimit: "minute";r=12;t=30, "day";r=0;t=3600. To back off, wait the largest t among the items whose r is 0, and never less than Retry-After when the response carries one (an ai_daily_limit 429 waits for the day's AI allowance, not the minute); both SDKs do exactly that.
A rate_limit_exceeded or daily_quota_exceeded 429 is in your request log (Dashboard → Logs, filter by its error code), like every refusal answered before your request runs: none of those is billed or counted as a request in your usage. An ai_daily_limit 429 is the operation's own answer and counts like one. In a storm the log keeps a bounded sample — up to 120 refusals a minute for each account on each of our servers — and the rest are answered but not written.
When you exceed the rate limit, the API responds with:
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710523260000
RateLimit-Policy: "minute";q=60;w=60
RateLimit: "minute";r=0;t=45
Content-Type: application/json
{
"status": "error",
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again later."
},
"request_id": "b3f1c2de-9a47-4e0b-8f5d-2c6a1e7d9b40"
}X-RateLimit-Reset header to know exactly when to retryImplement a retry strategy that respects the X-RateLimit-Reset header when available, and falls back to exponential backoff with jitter:
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
const resetAt = response.headers.get("X-RateLimit-Reset");
// X-RateLimit-Reset is a Unix timestamp in milliseconds.
const waitMs = resetAt
? Math.max(0, Number(resetAt) - Date.now())
: Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Rate limited. Retrying in ${Math.round(waitMs / 1000)}s...`);
await new Promise((r) => setTimeout(r, waitMs));
}
throw new Error("Max retries exceeded");
}X-RateLimit-Remaining on every response and throttle before hitting the limit.API reference · Troubleshooting · Email us at developers@printedsimply.com