> This page is for API.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.spreadspace.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.spreadspace.app/_mcp/server.

# Rate limits

Rate limits protect the platform and keep one integration's burst from
degrading another's. Limits are enforced per **(organization, API key)** with
a sliding 60-second window, so a burst at the tail of one minute does
not carry into the next.

## Policies

Different endpoints carry different budgets, each measured in requests per
minute, per API key:

| Endpoints                                                                           | Limit        |
| ----------------------------------------------------------------------------------- | ------------ |
| General reads and writes (jobs, borrowers, loans, extractions, deletes, …)          | **200/min**  |
| Uploads: presigned URLs, upload confirms, job status and download-URL reads, intake | **1000/min** |
| Exports: the synchronous single-document endpoint and creating an export            | **120/min**  |
| Sandbox reset and webhook delivery replay                                           | **10/min**   |

A signed-in user or an embed session gets 600 requests per minute on the
general lane; the figures above are per API key. Each reference page states
its lane.

The upload budget is sized around the batch endpoints, which is how a large
batch should move: `POST /api/documents/presigned-urls-batch` takes 100 files
per call, `POST /api/documents/confirm-uploads` 250 job ids, and
`POST /api/documents/status-batch` 200, so a 4,000-document batch is about
sixty requests plus polling. Note that the limit governs **API requests**, not
document processing throughput. Extraction is asynchronous and queued
independently.

There is also one fence per source IP address: 20,000 requests per five
minutes, counted across every key and every user behind that address. Past
it the edge answers **403** for up to five minutes, with no rate-limit
headers. It sits well above what a few keys and an office of workspace users share on
one egress; if your traffic can approach it, give each workload its own key
and, where you can, its own address.

Budgets are set per key by SpreadSpace and are not self-service: contact
support to raise the general budget for your integration. A raised budget
applies to the general lane only and shows in `RateLimit-Limit`
and `RateLimit-Policy` on every response it governs; the upload, export and
strict lanes keep their platform values.

## Response headers

Every response carries standard headers, except a `401` and a
`400 invalid_version`, which are answered before the request reaches a lane:

```
RateLimit-Limit: 200
RateLimit-Remaining: 137
RateLimit-Reset: 60            # seconds until a guaranteed-fresh window
RateLimit-Policy: 200;w=60
```

## When you exceed a limit

You get **429 Too Many Requests** with a `Retry-After` header (seconds) and
the standard [error envelope](/api/errors):

```json
{ "error": { "type": "rate_limited", "message": "Rate limit exceeded.", "request_id": "req_…" } }
```

A 429 can also come from a usage throttle rather than the per-minute limiter
(for example a workspace-level throttle); those responses carry `Retry-After`
too, and omit `RateLimit-Remaining` so a remaining budget is never implied.

Handle 429s by honoring `Retry-After`, with jitter:

```python
import random, time, requests

def request_with_backoff(method, url, *, max_attempts=5, **kwargs):
    for attempt in range(max_attempts):
        resp = requests.request(method, url, **kwargs)
        if resp.status_code != 429:
            return resp
        retry_after = int(resp.headers.get("Retry-After", "1"))
        time.sleep(retry_after + random.uniform(0, 0.5) * (attempt + 1))
    return resp
```

The official SDKs do this for you: they honor `Retry-After` and retry
automatically.

## Design guidance

* **Watch `RateLimit-Remaining`** and throttle proactively instead of reacting
  to 429s.
* **Batch where batch endpoints exist**: `POST /api/documents/status-batch`
  replaces N status polls; `POST /api/documents/confirm-uploads` replaces N
  confirms.
* **Prefer webhooks to polling** for job completion: a `job.completed` push
  costs you zero budget.
* **One key per workload** (backend, ETL, webhook receiver). Limits are
  per-key, so isolating workloads means a runaway ETL job can't starve your
  production ingest path, and gives each its own telemetry in the dashboard.