The Expand API rate-limits requests to keep usage fair across organizations.
How rate limiting works
Rate limits apply per organization. When you exceed your rate limit, the API returns a 429 Too Many Requests status.
Rate limit response
When you are rate limited, the API returns a 429 status with the error tag TooManyRequests and
a Retry-After header (in seconds):
{ "_tag": "TooManyRequests", "retryable": true}
retryable is always true here — the same request succeeds once the limit resets. Wait at least
as long as Retry-After asks before retrying. See Error Handling for the
field's full contract.
Handling rate limits
With the TypeScript SDK
The SDK retries rate limits for you: a 429 declares itself retryable, and the SDK waits out
Retry-After before trying again. You only need to handle the case where it runs out of attempts:
import { ExpandClient, ExpandClientApiError } from '@expandai/sdk'const client = new ExpandClient({ apiKey: '{{API_KEY}}', maxRetries: 5 })try { const result = await client.fetch({ url: 'https://example.com' })} catch (error) { if (error instanceof ExpandClientApiError && error.status === 429) { // Still rate limited after every retry — back off at the application level. console.log('Rate limited:', (error.body as { _tag?: string })?._tag)
Retry strategy
If you are calling the API directly rather than through an SDK, retry only when the body says to,
and never sooner than Retry-After asks:
Read retryable from the error body. If it is false, stop — no retry can succeed
Wait at least Retry-After seconds (or the body's retryAfterSeconds, if present)
Back off exponentially from there, up to a maximum (e.g., 32 seconds)
Jitter the wait. Every client rejected in the same burst is handed the same Retry-After, so an
unjittered wait sends the whole population back at one instant
After your maximum attempts, surface the error to the user
A successful /v1/fetch responds with text/markdown, not JSON — read it with response.text().
Only the error responses are JSON, and a failure from an intermediary (a proxy's own error page, an
empty body) may not be JSON either, so parsing it has to be allowed to fail.
async function fetchWithRetry(url: string, maxRetries = 5) { let delay = 1000 for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch('https://api.expand.ai/v1/fetch', { method: 'POST', headers: { 'x-expand-api-key': API_KEY, 'content-type': 'application/json' }, body: JSON.stringify({ url }), }) // The success body is the Markdown document itself.
Best practices
Batch requests where you can to cut down on API calls.
Cache responses instead of re-fetching the same URL.