Rate limits

Per-key budgets, standard headers, and how to back off.

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 can’t double-dip into the next.

Policies

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

EndpointsLimit
General reads and writes (jobs, borrowers, loans, extractions, deletes, …)200/min
Uploads: presigned URLs, upload confirms, job status and download-URL reads, intake1000/min
Exports, both the synchronous single-document endpoint and the async export operation120/min
Sandbox reset and webhook delivery replay10/min

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 a browser office share on one egress; if your traffic can approach it, give each workload its own key and, where you can, its own address.

Need more? Rate limits are set by SpreadSpace per key. Contact us and we’ll raise the general budget for your integration. They are not self-service.

Response headers

Every response carries standard headers:

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

Legacy X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset (epoch-seconds reset) are emitted alongside during a deprecation window. Prefer the unprefixed names.

When you exceed a limit

You get 429 Too Many Requests with a Retry-After header (seconds) and the standard error envelope:

1{ "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:

1import random, time, requests
2
3def request_with_backoff(method, url, *, max_attempts=5, **kwargs):
4 for attempt in range(max_attempts):
5 resp = requests.request(method, url, **kwargs)
6 if resp.status_code != 429:
7 return resp
8 retry_after = int(resp.headers.get("Retry-After", "1"))
9 time.sleep(retry_after + random.uniform(0, 0.5) * (attempt + 1))
10 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.