Skip to navigation

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 does not carry 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: the synchronous single-document endpoint and creating an export120/min
Sandbox reset and webhook delivery replay10/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:

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

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.