Developers
Rate Limits and Pagination
Per-plan rate limits, the 429 response, the Retry-After header, and the cursor-based pagination model.
Rate Limits and Pagination
Rate limits
Per-tenant, per-API-key quotas. Limits by plan:
| Plan | Requests / hour | Burst |
|---|---|---|
| Free | 100 | 20 |
| Starter | 1,000 | 100 |
| Business | 10,000 | 500 |
| Enterprise | Negotiated | Negotiated |
Limits are computed over a 1-hour sliding window. The burst budget allows short spikes above steady-state.
Rate limit headers
Every response includes:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9842
X-RateLimit-Reset: 1715612400
X-RateLimit-Reset is a Unix timestamp.
When you hit the limit
A 429 response:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{
"success": false,
"errors": [
{ "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded. Retry in 12 seconds." }
]
}
Retry-After is seconds. Sleep that long, retry.
Pagination
Cursor-based for stability under inserts. Initial request:
curl "https://yourtenant.papyrus.io/api/v1/documents?limit=50" \
-H "Authorization: Bearer $KEY" -H "X-Tenant-Id: $TENANT"
Response:
{
"success": true,
"data": [ /* 50 documents */ ],
"meta": {
"pagination": {
"limit": 50,
"hasMore": true,
"nextCursor": "eyJpZCI6IjE3MzkifQ=="
}
}
}
Next page:
curl "https://yourtenant.papyrus.io/api/v1/documents?limit=50&cursor=eyJpZCI6IjE3MzkifQ==" \
-H "Authorization: Bearer $KEY" -H "X-Tenant-Id: $TENANT"
When hasMore is false, you've reached the end.
Pagination limits
- Maximum
limitper request: 200 (default 50) - Cursors are valid for 1 hour
- Cursors are opaque — don't try to parse them; their format may change
Pull-everything pattern
async function* allDocuments(filters: Record<string, string>) {
let cursor: string | undefined;
do {
const url = new URL('https://yourtenant.papyrus.io/api/v1/documents');
Object.entries(filters).forEach(([k, v]) => url.searchParams.set(k, v));
url.searchParams.set('limit', '200');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers });
if (res.status === 429) {
await new Promise(r => setTimeout(r, parseInt(res.headers.get('Retry-After') || '60') * 1000));
continue;
}
const body = await res.json();
for (const doc of body.data) yield doc;
cursor = body.meta.pagination.hasMore ? body.meta.pagination.nextCursor : undefined;
} while (cursor);
}