# Rate limits

Source: https://portal.printedsimply.com/docs/rate-limits

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.

## Overview

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.

## Rate limit tiers

| 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](https://portal.printedsimply.com/pricing) for full plan details. Every response states your own limit in `X-RateLimit-Limit`.

## What the Free plan includes

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.

## Response headers

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: 1710523200000
```

## RateLimit and RateLimit-Policy

The 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=18
```

A `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.

## Handling 429 responses

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"
}
```

- Read the `X-RateLimit-Reset` header to know exactly when to retry
- Use exponential backoff with jitter as a fallback strategy
- Never retry in a tight loop without a delay

## Exponential backoff

Implement a retry strategy that respects the `X-RateLimit-Reset` header when available, and falls back to exponential backoff with jitter:

JavaScript:

```javascript
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");
}
```

Python:

```python
import time
import random
import requests

def fetch_with_retry(url: str, headers: dict, max_retries: int = 5):
    for attempt in range(max_retries + 1):
        response = requests.get(url, headers=headers)

        if response.status_code != 429:
            return response

        reset_at = response.headers.get("X-RateLimit-Reset")
        if reset_at:
            # X-RateLimit-Reset is a Unix timestamp in milliseconds.
            wait = max(0, float(reset_at) / 1000 - time.time())
        else:
            wait = (2 ** attempt) + random.random()

        print(f"Rate limited. Retrying in {wait:.1f}s...")
        time.sleep(wait)

    raise Exception("Max retries exceeded")
```

## Best practices

- Monitor headers proactively — check `X-RateLimit-Remaining` on every response and throttle before hitting the limit.
- Cache responses — avoid redundant API calls by caching product catalogs, shipping rates, and other slowly-changing data.
- Use webhooks — subscribe to [webhook events](https://portal.printedsimply.com/docs/webhooks) instead of polling for order status updates.
- Batch where possible — group related operations to minimize the number of requests.
- Spread traffic — avoid sending many requests simultaneously; stagger them over time.

[API reference](https://portal.printedsimply.com/docs/api-reference) · [Troubleshooting](https://portal.printedsimply.com/docs/troubleshooting) · Email us at developers@printedsimply.com
