Skip to main content
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 limit per 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);
}

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.