Rate limits

To keep the service fast for everyone, conversion requests (POST /v1/convert and POST /v1/convert/batch) are limited per account per minute, whatever the size of the files. Reading jobs, usage and keys, and GET /health, are not rate limited at the moment.

The limits

Tier Conversion requests per minute
Free 20 requests
Higher tiers up to 300 requests

GET /v1/usage tells you your own limit in rate_limit_per_minute. The limit counts requests, not pages: a batch of 100 files is one request.

What happens when you exceed it

The request is refused with 429 Too Many Requests and these headers:

Header Meaning
Retry-After Seconds to wait before trying again
X-RateLimit-Limit Your limit per minute
X-RateLimit-Remaining 0

The X-RateLimit-* headers are sent on 429 answers only, not on every response.

How to stay within the limit

  • Use batches instead of many single requests when you have many files.
  • Use asynchronous mode and webhooks instead of polling a job in a tight loop.
  • Wait for Retry-After when you get a 429, then retry. Do not retry immediately in a loop.
  • Spread bursts over time, for example with a small queue in your application.

A polite retry

The example retries after a 429 or a 503 (a provider that is busy), waits for the time the API asks for, and stops with a clear message on 402 (quota used up):

Handle quota, rate limits and busy providers
# curl retries 429 and 503 by itself, waiting for the Retry-After the API sends
curl -s --retry 3 --fail-with-body https://api.docslice.app/v1/convert \
  -H "Authorization: Bearer $DOCSLICE_API_KEY" \
  -F "file=@invoice.pdf" \
  -F "format=json"
import json
import os
import time
import requests

api_key = os.environ["DOCSLICE_API_KEY"]


def convert(path):
    for attempt in range(4):
        with open(path, "rb") as handle:
            response = requests.post(
                "https://api.docslice.app/v1/convert",
                headers={"Authorization": f"Bearer {api_key}"},
                files={"file": (path, handle)},
                timeout=120,
            )
        if response.status_code in (429, 503):  # too many requests, or the provider is busy: wait and retry
            time.sleep(int(response.headers.get("Retry-After", "5")))
            continue
        if response.status_code == 402:
            raise SystemExit("The monthly page quota is used up")
        response.raise_for_status()
        return response.json()
    raise SystemExit("The service stayed busy: try again later")


print(json.dumps(convert("invoice.pdf"), indent=2))
const fs = require("node:fs");

async function convert(path) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const form = new FormData();
    form.append("file", new Blob([fs.readFileSync(path)]), path);
    const response = await fetch("https://api.docslice.app/v1/convert", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.DOCSLICE_API_KEY}` },
      body: form,
    });
    if (response.status === 429 || response.status === 503) {
      // too many requests, or the provider is busy: wait and retry
      await new Promise((resolve) => setTimeout(resolve, 1000 * Number(response.headers.get("Retry-After") || 5)));
      continue;
    }
    if (response.status === 402) throw new Error("The monthly page quota is used up");
    if (!response.ok) throw new Error(`HTTP ${response.status}: ${JSON.stringify(await response.json())}`);
    return response.json();
  }
  throw new Error("The service stayed busy: try again later");
}

convert("invoice.pdf").then((data) => console.log(JSON.stringify(data, null, 2)));
<?php
function convert($path) {
    for ($attempt = 0; $attempt < 4; $attempt++) {
        $retryAfter = 5;
        $curl = curl_init('https://api.docslice.app/v1/convert');
        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('DOCSLICE_API_KEY')],
            CURLOPT_POSTFIELDS => ['file' => new CURLFile($path)],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 120,
            CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$retryAfter) {
                if (stripos($line, 'Retry-After:') === 0) {
                    $retryAfter = (int) trim(substr($line, 12));
                }
                return strlen($line);
            },
        ]);
        $body = curl_exec($curl);
        $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
        curl_close($curl);
        if ($status === 429 || $status === 503) {  // too many requests, or the provider is busy: wait and retry
            sleep($retryAfter);
            continue;
        }
        if ($status === 402) {
            exit("The monthly page quota is used up\n");
        }
        if ($status >= 400) {
            exit("HTTP $status: $body\n");
        }
        return json_decode($body, true);
    }
    exit("The service stayed busy: try again later\n");
}

echo json_encode(convert('invoice.pdf'), JSON_PRETTY_PRINT);

More on error handling in Errors and troubleshooting.