Document OCR
Upload a PDF or an image, run OCR, and get extracted text or a searchable PDF back. Five steps take a file to its result.
How does the FastOCR Document OCR API work?
The FastOCR Document OCR API extracts text and creates searchable PDFs from scanned and digital PDF files up to 1 GiB, and from PNG, JPG, GIF, WebP and BMP images up to 20 MiB. It supports 100+ languages. The API uses an asynchronous REST flow. Send a POST request with an idempotency key. The API returns a presigned S3 upload URL. Upload the file to that URL. Start processing. Poll the document status. Get the result as plain text or as a searchable PDF. Each page counts against the page allowance of the account, and an image counts as one page. By default, an account processes no more than 2 documents at the same time. Any signed-in FastOCR account can create an API key and start right away. The Free plan includes 20 free pages per month, shared with PDFs on the website. Paid plans share their Document OCR allowance with the API too.
Docs menu
On this page
Quickstart
Install the Python SDK, or use Node.js or plain HTTP. Choose a language in any code block. The page remembers your choice. Every example reads the API key from FASTOCR_API_KEY.
pip install fastocr-sdk
export FASTOCR_API_KEY="your_api_key"from fastocr_sdk import FastOCR
# Reads the key from the FASTOCR_API_KEY environment variable.
with FastOCR() as client:
text = client.extract_text("scan.pdf")
print(text)// Node.js 18+, no dependencies. Save as quickstart.mjs and run: node quickstart.mjs
import { readFile, writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
const BASE = 'https://api.fastocr.org';
const auth = { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` };
async function api(path, init = {}) {
const response = await fetch(BASE + path, { ...init, headers: { ...auth, ...init.headers } });
const body = await response.json();
if (!response.ok) throw new Error(`${response.status} ${body.error?.code ?? body.message}`);
return body;
}
const pdf = await readFile('scan.pdf');
// 1. Create a document
const created = await api('/v1/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID() },
body: JSON.stringify({ filename: 'scan.pdf', size_bytes: pdf.length }),
});
// 2. Upload the file to the presigned URL
const upload = await fetch(created.upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'application/pdf' },
body: pdf,
});
if (!upload.ok) throw new Error(`Upload failed: ${upload.status}`);
// 3. Start processing
await api(`/v1/documents/${created.id}/start`, { method: 'POST' });
// 4. Poll until the status is completed, partial, or failed
let document;
do {
await new Promise((resolve) => setTimeout(resolve, 2000));
document = await api(`/v1/documents/${created.id}`);
} while (!['completed', 'partial', 'failed'].includes(document.status));
if (document.status === 'failed') throw new Error(document.error.code);
// 5. Get the download link, then download the text
const output = await api(`/v1/documents/${created.id}/output?format=text`);
await writeFile('result.txt', await (await fetch(output.url)).text());
console.log(document.status, document.pages_billed);export FASTOCR_API_KEY="your_api_key"
# 1. Create a document
RESPONSE=$(curl -sS --fail-with-body -X POST https://api.fastocr.org/v1/documents \
-H "Authorization: Bearer $FASTOCR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filename":"scan.pdf","size_bytes":'$(wc -c < scan.pdf | tr -d ' ')'}')
DOCUMENT_ID=$(echo "$RESPONSE" | jq -r .id)
UPLOAD_URL=$(echo "$RESPONSE" | jq -r .upload_url)
# 2. Upload the file to the presigned URL
curl -sS --fail-with-body -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @scan.pdf
# 3. Start processing
curl -sS --fail-with-body -X POST https://api.fastocr.org/v1/documents/$DOCUMENT_ID/start \
-H "Authorization: Bearer $FASTOCR_API_KEY"
# 4. Poll until the status is completed, partial, or failed
while true; do
BODY=$(curl -sS --fail-with-body https://api.fastocr.org/v1/documents/$DOCUMENT_ID \
-H "Authorization: Bearer $FASTOCR_API_KEY") || exit 1
STATUS=$(echo "$BODY" | jq -r .status)
case "$STATUS" in completed|partial|failed) break;; esac
sleep 2
done
echo "Final status: $STATUS"
# 5. Get the download link, then download the text (a failed document has no output)
RESULT_URL=$(curl -sS --fail-with-body "https://api.fastocr.org/v1/documents/$DOCUMENT_ID/output?format=text" \
-H "Authorization: Bearer $FASTOCR_API_KEY" | jq -r .url)
curl -sS --fail-with-body "$RESULT_URL" -o result.txtextract_text uploads the file, starts OCR, waits for the result, and returns the text. The Node.js and HTTP tabs show the same five steps.
Keep the searchable PDF
from fastocr_sdk import FastOCR
with FastOCR() as client:
job = client.documents.process("scan.pdf")
client.documents.download_searchable_pdf(job.id, "searchable.pdf")
print(job.id, job.status, job.pages_billed)import { writeFile } from 'node:fs/promises';
const documentId = '3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47';
const response = await fetch(`https://api.fastocr.org/v1/documents/${documentId}/output?format=pdf`, {
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
const { url } = await response.json();
const pdf = await fetch(url);
await writeFile('searchable.pdf', Buffer.from(await pdf.arrayBuffer()));# Get the download link for the searchable PDF
RESULT_URL=$(curl -s "https://api.fastocr.org/v1/documents/$DOCUMENT_ID/output?format=pdf" \
-H "Authorization: Bearer $FASTOCR_API_KEY" | jq -r .url)
# Download the file
curl -s "$RESULT_URL" -o searchable.pdfAccess
Any signed-in FastOCR account can create an API key and start right away. The Free plan includes 20 free pages per month, shared with PDFs on the website. Paid plans share their Document OCR allowance with the API too. See Getting access.
- Sign in and create an API key in the developer console. You accept the API terms with your first key.
- Run the quickstart with your key in FASTOCR_API_KEY.
- When you need more document pages, buy a Page Pack on the pricing page.
Accepted files: PDF files up to 1 GiB, and PNG, JPG, JPEG, GIF, WebP and BMP images up to 20 MiB. An image counts as one page. Other file types return 400 unsupported_file_type.
Output formats: ?format=text gives plain text. ?format=pdf gives a searchable PDF.
Statuses: awaiting_upload → processing → completed or failed. If the account has too few pages, the document stops after the first pages. It then finishes as partial with is_truncated: true. There is no deleting status.
POST/v1/documents
Create a document and get a presigned upload URL.
Required headers
Idempotency-Key: a unique value for each document, for example a UUID. The API matches the header name without regard to case. See Idempotency.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| Fieldfilename | Typestring | RequiredYes | DescriptionThe name must end with .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp (any case) and have at most 255 characters. The extension sets the file type and the Content-Type of the upload. |
| Fieldsize_bytes | Typeinteger | RequiredYes | DescriptionThe exact file size in bytes. The maximum is 1,073,741,824 (1 GiB) for a PDF and 20,971,520 (20 MiB) for an image. If the upload size is different, start returns 400 size_mismatch. |
| Fieldsha256 | Typestring | RequiredNo | DescriptionThe SHA-256 of the file, as base64 of the 32 raw digest bytes. This is the encoding that S3 uses for x-amz-checksum-sha256. A value in another encoding, such as hex, returns 400 invalid_sha256. The API attaches the checksum to the upload URL, and S3 checks it on upload. |
| Fieldexternal_id | Typestring | RequiredNo | DescriptionYour own correlation value, at most 256 characters. The API stores it and returns it as external_id. It does not need to be unique. A value that is not a string of that length returns 400 invalid_external_id. |
Example request
from fastocr_sdk import FastOCR
with FastOCR() as client:
created = client.documents.create("scan.pdf", 1048576, external_id="INV-1001")
print(created.id, created.upload_url)import { randomUUID } from 'node:crypto';
const response = await fetch('https://api.fastocr.org/v1/documents', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FASTOCR_API_KEY}`,
'Idempotency-Key': randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({ filename: 'scan.pdf', size_bytes: 1048576, external_id: 'INV-1001' }),
});
const created = await response.json();
console.log(created.id, created.upload_url);curl -s -X POST https://api.fastocr.org/v1/documents \
-H "Authorization: Bearer $FASTOCR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filename":"scan.pdf","size_bytes":1048576,"external_id":"INV-1001"}'Response
{
"id": "3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47",
"status": "awaiting_upload",
"upload_url": "https://...s3.amazonaws.com/...",
"expires_at": "2026-10-01T13:00:00.123456+00:00"
}{
"error": {
"type": "invalid_request",
"code": "invalid_sha256",
"message": "sha256 must be the base64 encoding of the 32-byte SHA-256 digest",
"request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef"
}
}id is a UUID. Send the file with PUT to upload_url, with the Content-Type of its extension (for a PDF, application/pdf). The upload URL is signed for that type, so S3 rejects any other. Do this before expires_at (one hour).
| Extension | Content-Type |
|---|---|
| Extension.pdf | Content-Typeapplication/pdf |
| Extension.png | Content-Typeimage/png |
| Extension.jpg, .jpeg | Content-Typeimage/jpeg |
| Extension.gif | Content-Typeimage/gif |
| Extension.webp | Content-Typeimage/webp |
| Extension.bmp | Content-Typeimage/bmp |
client.documents.upload(created.upload_url, "scan.pdf")import { readFile } from 'node:fs/promises';
await fetch(created.upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'application/pdf' },
body: await readFile('scan.pdf'),
});curl -s -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
-T scan.pdfA client can repeat the request with the same Idempotency-Key and body within about 24 hours. The API then returns the original 201. If the original upload_url expired while the document is still awaiting_upload, the response carries a fresh upload_url and expires_at.
Errors
| HTTP | Code | Type | When |
|---|---|---|---|
| HTTP400 | Codeinvalid_json | Typeinvalid_request | WhenThe request body is not valid JSON, or it is not a JSON object. |
| HTTP400 | Codemissing_idempotency_key | Typeinvalid_request | WhenA create request has no Idempotency-Key header. |
| HTTP400 | Codeinvalid_idempotency_key | Typeinvalid_request | WhenThe Idempotency-Key header is longer than 255 characters. |
| HTTP400 | Codeinvalid_filename | Typeinvalid_request | Whenfilename is missing, is not a string, or is longer than 255 characters. |
| HTTP400 | Codeunsupported_file_type | Typeinvalid_request | Whenfilename does not end in .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp. |
| HTTP400 | Codeinvalid_size_bytes | Typeinvalid_request | Whensize_bytes is not a positive integer. |
| HTTP400 | Codefile_too_large | Typeinvalid_request | Whensize_bytes is more than 1,073,741,824 (1 GiB) for a PDF, or more than 20,971,520 (20 MiB) for an image. |
| HTTP400 | Codeinvalid_sha256 | Typeinvalid_request | Whensha256 is not the base64 encoding of the 32-byte digest. |
| HTTP400 | Codeinvalid_external_id | Typeinvalid_request | Whenexternal_id is not a string of at most 256 characters. |
| HTTP409 | Codeidempotency_key_conflict | Typeconflict | WhenA request used the key before with a different body. |
GET/v1/documents
List your documents, with optional filters. See Pagination for the query parameters.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| Fieldlimit | Typeinteger | RequiredNo | DescriptionAn integer from 1 to 100. The default is 20. An invalid value returns 400 invalid_limit. |
| Fieldcursor | Typestring | RequiredNo | DescriptionThe next_cursor from the previous page. A cursor expires after 24 hours. An invalid or expired cursor returns 400 invalid_cursor. |
| Fieldstatus | Typestring | RequiredNo | DescriptionOne of awaiting_upload, processing, completed, partial, or failed. Any other value returns 400 invalid_status. |
| Fieldupdated_since | Typestring | RequiredNo | DescriptionReturns documents whose status changed at or after this value. If the status never changed, the creation time applies. Send an ISO 8601 date or datetime, for example 2026-10-01T00:00:00Z. A value with no offset is UTC. The API does not reject a value that is not a date. It compares that value as text. |
from fastocr_sdk import FastOCR
with FastOCR() as client:
page = client.documents.list(
limit=20,
status="completed",
updated_since="2026-10-01T00:00:00Z",
)
for document in page.documents:
print(document.id, document.status)
print(page.next_cursor)const params = new URLSearchParams({ status: 'completed', updated_since: '2026-10-01T00:00:00Z' });
const response = await fetch(`https://api.fastocr.org/v1/documents?${params}`, {
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
const page = await response.json();
for (const document of page.documents) console.log(document.id, document.status);
console.log(page.next_cursor);curl -s -G "https://api.fastocr.org/v1/documents" \
-H "Authorization: Bearer $FASTOCR_API_KEY" \
--data-urlencode "status=completed" \
--data-urlencode "updated_since=2026-10-01T00:00:00Z"The API returns documents newest first. Each page holds up to limit documents that match your filters. On a very long list, a page can be shorter while next_cursor has a value. Follow the cursor until it is null.
Response
{
"documents": [
{
"id": "3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47",
"status": "completed",
"external_id": "INV-1001",
"pages_billed": 10,
"pages_total": 10,
"pages_processed": 10,
"is_truncated": false,
"outputs": {
"text": { "available": true },
"pdf": { "available": true }
}
}
],
"next_cursor": null
}{
"error": {
"type": "invalid_request",
"code": "invalid_cursor",
"message": "Cursor is invalid or expired",
"request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef"
}
}GET/v1/documents/{id}
Get the current status and metadata of one document.
from fastocr_sdk import FastOCR
with FastOCR() as client:
job = client.documents.get("3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47")
print(job.status, job.pages_billed, job.is_truncated)const documentId = '3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47';
const response = await fetch(`https://api.fastocr.org/v1/documents/${documentId}`, {
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
const document = await response.json();
console.log(document.status, document.pages_billed, document.is_truncated);curl -s https://api.fastocr.org/v1/documents/$DOCUMENT_ID \
-H "Authorization: Bearer $FASTOCR_API_KEY"Response
{
"id": "3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47",
"status": "completed",
"external_id": "INV-1001",
"pages_billed": 10,
"pages_total": 10,
"pages_processed": 10,
"is_truncated": false,
"outputs": {
"text": { "available": true },
"pdf": { "available": true }
}
}{
"id": "3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47",
"status": "failed",
"external_id": null,
"error": {
"type": "processing_error",
"code": "timeout",
"message": "This file took too long to process. Please try again.",
"retryable": true
}
}A failed document still returns 200. Read error.code to find the cause.
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| Fieldid | Typestring | RequiredYes | DescriptionThe document ID. |
| Fieldstatus | Typestring | RequiredYes | DescriptionOne of awaiting_upload, processing, completed, partial, or failed. |
| Fieldexternal_id | Typestring? | RequiredNo | DescriptionYour correlation ID, if you set it on create. |
| Fieldpages_billed | Typeinteger | RequiredNo | DescriptionThe pages that the API charged to your page allowance. This field is present after the API reserves pages. |
| Fieldpages_total | Typeinteger | RequiredNo | DescriptionThe pages in the uploaded PDF, when known. An image is 1 page. |
| Fieldpages_processed | Typeinteger | RequiredNo | DescriptionThe pages that the API processed, when it reports them. |
| Fieldis_truncated | Typeboolean | RequiredNo | Descriptiontrue if the API processed fewer pages than the PDF has. |
| Fieldoutputs | Typeobject | RequiredNo | DescriptionThe available output formats (text, pdf). |
| Fielderror | Typeobject | RequiredNo | DescriptionPresent if the document failed. See Errors. |
If the document does not exist for your account, the API returns 404 document_not_found.
POST/v1/documents/{id}/start
Start OCR processing after you upload the file. The request has no body.
from fastocr_sdk import FastOCR
with FastOCR() as client:
client.documents.start("3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47")const documentId = '3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47';
const response = await fetch(`https://api.fastocr.org/v1/documents/${documentId}/start`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
console.log(response.status, await response.json());curl -s -X POST https://api.fastocr.org/v1/documents/$DOCUMENT_ID/start \
-H "Authorization: Bearer $FASTOCR_API_KEY"Response
{
"status": "processing"
}{
"error": {
"type": "conflict",
"code": "document_not_startable",
"message": "Document cannot be started from its current state",
"request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef"
}
}A POST /start request for a document that is already in flight returns 202 with {"status":"processing"}. You can start a document that is awaiting upload, and a document that failed with buy_pages.
Errors
| HTTP | Code | Type | When |
|---|---|---|---|
| HTTP400 | Codeupload_not_found | Typeinvalid_request | WhenA start request came before the file upload. |
| HTTP400 | Codesize_mismatch | Typeinvalid_request | WhenThe size of the uploaded file is different from size_bytes. |
| HTTP404 | Codedocument_not_found | Typenot_found | WhenThis account has no such document. |
| HTTP409 | Codedocument_not_startable | Typeconflict | WhenThe document is completed, partial, or failed with a code other than buy_pages. |
| HTTP429 | Codeconcurrency_limit | Typerate_limit | WhenToo many documents are in flight. Wait the Retry-After seconds (30), then call POST /start again. |
buy_pages after the API knows the page count. Buy pages as Page Packs; they work on the website too. After you buy pages, call POST /start again on the same document.GET/v1/documents/{id}/output
Get a presigned download URL for the processed result.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| Fieldformat | Typestring | RequiredYes | DescriptionUse text for a plain-text file. Use pdf for a searchable PDF (for an image, a one-page PDF of the image with a text layer). Any other value returns 400 invalid_format. |
from fastocr_sdk import FastOCR
with FastOCR() as client:
document_id = "3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47"
link = client.documents.get_output_url(document_id, "text")
print(link.url, link.expires_at)
text = client.documents.get_text(document_id)
client.documents.download_searchable_pdf(document_id, "searchable.pdf")const documentId = '3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47';
const response = await fetch(`https://api.fastocr.org/v1/documents/${documentId}/output?format=text`, {
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
const { url, expires_at } = await response.json();
const text = await (await fetch(url)).text();curl -s "https://api.fastocr.org/v1/documents/$DOCUMENT_ID/output?format=text" \
-H "Authorization: Bearer $FASTOCR_API_KEY"Response
{
"format": "text",
"url": "https://...s3.amazonaws.com/...",
"expires_at": "2026-10-01T13:00:00.123456+00:00",
"pages_billed": 10,
"pages_total": 10
}{
"error": {
"type": "conflict",
"code": "document_not_ready",
"message": "Document is not ready",
"request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef"
}
}The presigned URL expires in 1 hour. After that, request a new one. If the document is not complete, or if it failed, the API returns 409 document_not_ready.
DELETE/v1/documents/{id}
Delete a document and its files. Deletion is asynchronous. The request returns 202. The API deletes the files and the document record in the background.
GET never returns a deleting status. Until the API deletes the record, the document keeps its last status. After that, GET and DELETE return 404 document_not_found.
from fastocr_sdk import FastOCR
with FastOCR() as client:
client.documents.delete("3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47")const documentId = '3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47';
const response = await fetch(`https://api.fastocr.org/v1/documents/${documentId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
console.log(response.status, await response.json());curl -s -X DELETE https://api.fastocr.org/v1/documents/$DOCUMENT_ID \
-H "Authorization: Bearer $FASTOCR_API_KEY"Response
{
"status": "deleting"
}{
"error": {
"type": "conflict",
"code": "document_processing",
"message": "Processing documents cannot be deleted",
"request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef"
}
}If the document is in flight, the API returns 409 document_processing. If you repeat the request while deletion is pending, the API returns 202 again.
GET/v1/balance
Get the pages the account can still use. Document OCR on the website and API shares your plan’s monthly allowance, including trial and unlimited plans, then uses purchased Page Packs. Bank statement pages are separate. Use the document_pages summary; older free and contract fields remain for compatibility.
const response = await fetch('https://api.fastocr.org/v1/balance', {
headers: { Authorization: `Bearer ${process.env.FASTOCR_API_KEY}` },
});
const balance = await response.json();
console.log(balance.document_pages);curl -s https://api.fastocr.org/v1/balance \
-H "Authorization: Bearer $FASTOCR_API_KEY"Response
{
"object": "balance",
"pages_remaining": 2480,
"free_pages_per_month": 20,
"free_pages_remaining": 15,
"free_pages_reset_at": "2026-11-01T00:00:00+00:00",
"contract_pages_per_month": null,
"contract_pages_remaining": null,
"statement_pages_remaining": 0,
"document_pages": {
"monthly_limit": 20,
"monthly_used": 5,
"monthly_remaining": 15,
"purchased_remaining": 2480,
"purchased_expires_at": null,
"total_remaining": 2495,
"is_unlimited": false,
"enforced": true,
"resets_at": "2026-11-01T00:00:00+00:00"
}
}| Field | Description |
|---|---|
| Fielddocument_pages | DescriptionAuthoritative shared Document OCR allowance: monthly limit, combined website/API usage, monthly remaining, purchased remaining, the earliest purchased-page expiry (null when none expire), total remaining, enforcement, and the UTC reset time (the monthly billing date on paid plans, the 1st of the month on free accounts). An uncapped total is null with is_unlimited: true. |
| Fieldobject | DescriptionAlways balance. |
| Fieldpages_remaining | DescriptionPages left in all Page Packs. These pages also work on the website. |
| Fieldfree_pages_per_month | DescriptionLegacy compatibility field; use document_pages. Free pages every account gets each month. PDFs on the website share them. |
| Fieldfree_pages_remaining | DescriptionLegacy compatibility field; use document_pages. Free pages left this month. |
| Fieldfree_pages_reset_at | DescriptionLegacy compatibility field; use document_pages. When the monthly pages reset, as an ISO 8601 UTC time. |
| Fieldcontract_pages_per_month | DescriptionLegacy compatibility field; use document_pages. Pages a month under an API contract, or null without one. |
| Fieldcontract_pages_remaining | DescriptionLegacy compatibility field; use document_pages. Contract pages left this month, or null without a contract. |
| Fieldstatement_pages_remaining | DescriptionBank statement pages left. The API cannot process bank statements; the website uses these. |
Errors
A rejected request returns an error object with type, code, message, and request_id. A document that fails during processing returns 200 with an error object on the document. Match on code, and treat an unknown code as new.
Document failure codes
| Code | Type | When | Retryable |
|---|---|---|---|
| Codebuy_pages | Typeinsufficient_credits | WhenThe account has no pages left. | RetryableNo. Buy a Page Pack. Then call POST /start again on the same document. |
| Codepassword_protected | Typeinvalid_request | WhenThe file has a password. | RetryableNo |
| Codeunreadable_document | Typeinvalid_request | WhenThe API could not read the PDF. | RetryableNo |
| Codeno_text_detected | Typeinvalid_request | WhenThe API found no readable text in the file. | RetryableNo |
| Codecorrupted_file | Typeinvalid_request | WhenThe file has damage, or the API does not support it. | RetryableNo |
| Codeimage_too_large | Typeinvalid_request | WhenThe image is too large or has too many pixels to read. Upload a smaller copy. | RetryableNo |
| Codetoo_many_pages | Typeinvalid_request | WhenThe file has more pages than one document may hold. | RetryableNo |
| Codeempty_file | Typeinvalid_request | WhenThe file has no pages. | RetryableNo |
| Codetimeout | Typeprocessing_error | WhenProcessing took too long. | RetryableYes |
| Codeout_of_memory | Typeprocessing_error | WhenProcessing used all available memory. | RetryableYes |
| Codeprocessing_failed | Typeprocessing_error | WhenAny other failure. | RetryableYes |
Request errors
| HTTP | Code | Type | When |
|---|---|---|---|
| HTTP400 | Codeinvalid_json | Typeinvalid_request | WhenThe request body is not valid JSON, or it is not a JSON object. |
| HTTP400 | Codemissing_idempotency_key | Typeinvalid_request | WhenA create request has no Idempotency-Key header. |
| HTTP400 | Codeinvalid_idempotency_key | Typeinvalid_request | WhenThe Idempotency-Key header is longer than 255 characters. |
| HTTP400 | Codeinvalid_filename | Typeinvalid_request | Whenfilename is missing, is not a string, or is longer than 255 characters. |
| HTTP400 | Codeunsupported_file_type | Typeinvalid_request | Whenfilename does not end in .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp. |
| HTTP400 | Codeinvalid_size_bytes | Typeinvalid_request | Whensize_bytes is not a positive integer. |
| HTTP400 | Codefile_too_large | Typeinvalid_request | Whensize_bytes is more than 1,073,741,824 (1 GiB) for a PDF, or more than 20,971,520 (20 MiB) for an image. |
| HTTP400 | Codeinvalid_sha256 | Typeinvalid_request | Whensha256 is not the base64 encoding of the 32-byte digest. |
| HTTP400 | Codeinvalid_external_id | Typeinvalid_request | Whenexternal_id is not a string of at most 256 characters. |
| HTTP400 | Codeupload_not_found | Typeinvalid_request | WhenA start request came before the file upload. |
| HTTP400 | Codesize_mismatch | Typeinvalid_request | WhenThe size of the uploaded file is different from size_bytes. |
| HTTP400 | Codeinvalid_format | Typeinvalid_request | Whenformat is not text or pdf. |
| HTTP400 | Codeinvalid_limit | Typeinvalid_request | Whenlimit is not an integer from 1 to 100. |
| HTTP400 | Codeinvalid_status | Typeinvalid_request | Whenstatus is not a public document status. |
| HTTP400 | Codeinvalid_cursor | Typeinvalid_request | Whencursor has the wrong format, or it is more than 24 hours old. |
| HTTP404 | Codedocument_not_found | Typenot_found | WhenThis account has no such document. |
| HTTP404 | Codeoutput_unavailable | Typenot_found | WhenA client requested format=pdf for a document whose searchable PDF could not be generated. The extracted text is still available with format=text. |
| HTTP409 | Codeidempotency_key_conflict | Typeconflict | WhenA request used the key before with a different body. |
| HTTP409 | Codedocument_not_startable | Typeconflict | WhenThe document is completed, partial, or failed with a code other than buy_pages. |
| HTTP409 | Codedocument_not_ready | Typeconflict | WhenA client requested output before the document completed, or the document failed. |
| HTTP409 | Codedocument_processing | Typeconflict | WhenA client sent DELETE for a document that is still in flight. |
| HTTP429 | Codeconcurrency_limit | Typerate_limit | WhenToo many documents are in flight. Wait the Retry-After seconds (30), then call POST /start again. |
| HTTP500 | Codenot stable | Typeerror | WhenUnexpected server error. Retry with backoff. |
Gateway errors
The API gateway rejects a request that has no valid API key before it reaches the API. The body is different:
HTTP 401
{
"message": "Unauthorized",
"error": { "type": "gateway_error", "code": "unauthorized", "message": "Unauthorized", "request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef" }
}
HTTP 403
{
"message": "User is not authorized to access this resource with an explicit deny in an identity-based policy",
"error": { "type": "gateway_error", "code": "access_denied", "message": "User is not authorized to access this resource with an explicit deny in an identity-based policy", "request_id": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef" }
}Both bodies always have a message. Its text can change. On newer deployments, the body also has an error object with type: "gateway_error". If error is absent, use the HTTP status and message.
| HTTP | Code | Type | When |
|---|---|---|---|
| HTTP401 | Codeunauthorized | Typegateway_error | WhenThe request has no Authorization header. |
| HTTP403 | Codeaccess_denied | Typegateway_error | WhenThe API key is unknown, malformed, or revoked. |
| HTTP429 | Codethrottled | Typegateway_error | WhenThe request is above the per-key or per-address rate limit on /v1, or the shared throttle of the API gateway. A per-key or per-address 429 has message only and a Retry-After header. Wait, then retry. |
| HTTP403 | Codemissing_authentication_token | Typegateway_error | WhenThe route does not exist and the request has no Authorization header. With a Bearer API key, the same request returns 403 gateway_error. |
| HTTPOther | Codegateway_error | Typegateway_error | WhenAny other gateway condition. Use the HTTP status and message. |