Skip to main content

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.

Python SDKfastocr-sdk 0.3.0PyPIGitHub

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.

Terminalbash
pip install fastocr-sdk
export FASTOCR_API_KEY="your_api_key"
Python
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)

extract_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

Python
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)

Access

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.

  1. Sign in and create an API key in the developer console. You accept the API terms with your first key.
  2. Run the quickstart with your key in FASTOCR_API_KEY.
  3. 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

FieldfilenameTypestringRequiredYesDescriptionThe 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_bytesTypeintegerRequiredYesDescriptionThe 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.
Fieldsha256TypestringRequiredNoDescriptionThe 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_idTypestringRequiredNoDescriptionYour 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

Python
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)

Response

JSON
{
  "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"
}

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.pdfContent-Typeapplication/pdf
Extension.pngContent-Typeimage/png
Extension.jpg, .jpegContent-Typeimage/jpeg
Extension.gifContent-Typeimage/gif
Extension.webpContent-Typeimage/webp
Extension.bmpContent-Typeimage/bmp
Python
client.documents.upload(created.upload_url, "scan.pdf")

A 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

HTTP400Codeinvalid_jsonTypeinvalid_requestWhenThe request body is not valid JSON, or it is not a JSON object.
HTTP400Codemissing_idempotency_keyTypeinvalid_requestWhenA create request has no Idempotency-Key header.
HTTP400Codeinvalid_idempotency_keyTypeinvalid_requestWhenThe Idempotency-Key header is longer than 255 characters.
HTTP400Codeinvalid_filenameTypeinvalid_requestWhenfilename is missing, is not a string, or is longer than 255 characters.
HTTP400Codeunsupported_file_typeTypeinvalid_requestWhenfilename does not end in .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp.
HTTP400Codeinvalid_size_bytesTypeinvalid_requestWhensize_bytes is not a positive integer.
HTTP400Codefile_too_largeTypeinvalid_requestWhensize_bytes is more than 1,073,741,824 (1 GiB) for a PDF, or more than 20,971,520 (20 MiB) for an image.
HTTP400Codeinvalid_sha256Typeinvalid_requestWhensha256 is not the base64 encoding of the 32-byte digest.
HTTP400Codeinvalid_external_idTypeinvalid_requestWhenexternal_id is not a string of at most 256 characters.
HTTP409Codeidempotency_key_conflictTypeconflictWhenA 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

FieldlimitTypeintegerRequiredNoDescriptionAn integer from 1 to 100. The default is 20. An invalid value returns 400 invalid_limit.
FieldcursorTypestringRequiredNoDescriptionThe next_cursor from the previous page. A cursor expires after 24 hours. An invalid or expired cursor returns 400 invalid_cursor.
FieldstatusTypestringRequiredNoDescriptionOne of awaiting_upload, processing, completed, partial, or failed. Any other value returns 400 invalid_status.
Fieldupdated_sinceTypestringRequiredNoDescriptionReturns 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.
Python
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)

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

JSON
{
  "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
}

GET/v1/documents/{id}

Get the current status and metadata of one document.

Python
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)

Response

JSON
{
  "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 }
  }
}

A failed document still returns 200. Read error.code to find the cause.

Response fields

FieldidTypestringRequiredYesDescriptionThe document ID.
FieldstatusTypestringRequiredYesDescriptionOne of awaiting_upload, processing, completed, partial, or failed.
Fieldexternal_idTypestring?RequiredNoDescriptionYour correlation ID, if you set it on create.
Fieldpages_billedTypeintegerRequiredNoDescriptionThe pages that the API charged to your page allowance. This field is present after the API reserves pages.
Fieldpages_totalTypeintegerRequiredNoDescriptionThe pages in the uploaded PDF, when known. An image is 1 page.
Fieldpages_processedTypeintegerRequiredNoDescriptionThe pages that the API processed, when it reports them.
Fieldis_truncatedTypebooleanRequiredNoDescriptiontrue if the API processed fewer pages than the PDF has.
FieldoutputsTypeobjectRequiredNoDescriptionThe available output formats (text, pdf).
FielderrorTypeobjectRequiredNoDescriptionPresent 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.

Python
from fastocr_sdk import FastOCR

with FastOCR() as client:
    client.documents.start("3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47")

Response

JSON
{
  "status": "processing"
}

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

HTTP400Codeupload_not_foundTypeinvalid_requestWhenA start request came before the file upload.
HTTP400Codesize_mismatchTypeinvalid_requestWhenThe size of the uploaded file is different from size_bytes.
HTTP404Codedocument_not_foundTypenot_foundWhenThis account has no such document.
HTTP409Codedocument_not_startableTypeconflictWhenThe document is completed, partial, or failed with a code other than buy_pages.
HTTP429Codeconcurrency_limitTyperate_limitWhenToo many documents are in flight. Wait the Retry-After seconds (30), then call POST /start again.
A document that has no pages left fails with 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

FieldformatTypestringRequiredYesDescriptionUse 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.
Python
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")

Response

JSON
{
  "format": "text",
  "url": "https://...s3.amazonaws.com/...",
  "expires_at": "2026-10-01T13:00:00.123456+00:00",
  "pages_billed": 10,
  "pages_total": 10
}

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.

Python
from fastocr_sdk import FastOCR

with FastOCR() as client:
    client.documents.delete("3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47")

Response

JSON
{
  "status": "deleting"
}

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.

JavaScript
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);

Response

200 OKJSON
{
  "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"
  }
}
Fielddocument_pagesDescriptionAuthoritative 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.
FieldobjectDescriptionAlways balance.
Fieldpages_remainingDescriptionPages left in all Page Packs. These pages also work on the website.
Fieldfree_pages_per_monthDescriptionLegacy compatibility field; use document_pages. Free pages every account gets each month. PDFs on the website share them.
Fieldfree_pages_remainingDescriptionLegacy compatibility field; use document_pages. Free pages left this month.
Fieldfree_pages_reset_atDescriptionLegacy compatibility field; use document_pages. When the monthly pages reset, as an ISO 8601 UTC time.
Fieldcontract_pages_per_monthDescriptionLegacy compatibility field; use document_pages. Pages a month under an API contract, or null without one.
Fieldcontract_pages_remainingDescriptionLegacy compatibility field; use document_pages. Contract pages left this month, or null without a contract.
Fieldstatement_pages_remainingDescriptionBank 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

Codebuy_pagesTypeinsufficient_creditsWhenThe account has no pages left.RetryableNo. Buy a Page Pack. Then call POST /start again on the same document.
Codepassword_protectedTypeinvalid_requestWhenThe file has a password.RetryableNo
Codeunreadable_documentTypeinvalid_requestWhenThe API could not read the PDF.RetryableNo
Codeno_text_detectedTypeinvalid_requestWhenThe API found no readable text in the file.RetryableNo
Codecorrupted_fileTypeinvalid_requestWhenThe file has damage, or the API does not support it.RetryableNo
Codeimage_too_largeTypeinvalid_requestWhenThe image is too large or has too many pixels to read. Upload a smaller copy.RetryableNo
Codetoo_many_pagesTypeinvalid_requestWhenThe file has more pages than one document may hold.RetryableNo
Codeempty_fileTypeinvalid_requestWhenThe file has no pages.RetryableNo
CodetimeoutTypeprocessing_errorWhenProcessing took too long.RetryableYes
Codeout_of_memoryTypeprocessing_errorWhenProcessing used all available memory.RetryableYes
Codeprocessing_failedTypeprocessing_errorWhenAny other failure.RetryableYes

Request errors

HTTP400Codeinvalid_jsonTypeinvalid_requestWhenThe request body is not valid JSON, or it is not a JSON object.
HTTP400Codemissing_idempotency_keyTypeinvalid_requestWhenA create request has no Idempotency-Key header.
HTTP400Codeinvalid_idempotency_keyTypeinvalid_requestWhenThe Idempotency-Key header is longer than 255 characters.
HTTP400Codeinvalid_filenameTypeinvalid_requestWhenfilename is missing, is not a string, or is longer than 255 characters.
HTTP400Codeunsupported_file_typeTypeinvalid_requestWhenfilename does not end in .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp.
HTTP400Codeinvalid_size_bytesTypeinvalid_requestWhensize_bytes is not a positive integer.
HTTP400Codefile_too_largeTypeinvalid_requestWhensize_bytes is more than 1,073,741,824 (1 GiB) for a PDF, or more than 20,971,520 (20 MiB) for an image.
HTTP400Codeinvalid_sha256Typeinvalid_requestWhensha256 is not the base64 encoding of the 32-byte digest.
HTTP400Codeinvalid_external_idTypeinvalid_requestWhenexternal_id is not a string of at most 256 characters.
HTTP400Codeupload_not_foundTypeinvalid_requestWhenA start request came before the file upload.
HTTP400Codesize_mismatchTypeinvalid_requestWhenThe size of the uploaded file is different from size_bytes.
HTTP400Codeinvalid_formatTypeinvalid_requestWhenformat is not text or pdf.
HTTP400Codeinvalid_limitTypeinvalid_requestWhenlimit is not an integer from 1 to 100.
HTTP400Codeinvalid_statusTypeinvalid_requestWhenstatus is not a public document status.
HTTP400Codeinvalid_cursorTypeinvalid_requestWhencursor has the wrong format, or it is more than 24 hours old.
HTTP404Codedocument_not_foundTypenot_foundWhenThis account has no such document.
HTTP404Codeoutput_unavailableTypenot_foundWhenA client requested format=pdf for a document whose searchable PDF could not be generated. The extracted text is still available with format=text.
HTTP409Codeidempotency_key_conflictTypeconflictWhenA request used the key before with a different body.
HTTP409Codedocument_not_startableTypeconflictWhenThe document is completed, partial, or failed with a code other than buy_pages.
HTTP409Codedocument_not_readyTypeconflictWhenA client requested output before the document completed, or the document failed.
HTTP409Codedocument_processingTypeconflictWhenA client sent DELETE for a document that is still in flight.
HTTP429Codeconcurrency_limitTyperate_limitWhenToo many documents are in flight. Wait the Retry-After seconds (30), then call POST /start again.
HTTP500Codenot stableTypeerrorWhenUnexpected 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:

Authentication errorsHTTP
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.

HTTP401CodeunauthorizedTypegateway_errorWhenThe request has no Authorization header.
HTTP403Codeaccess_deniedTypegateway_errorWhenThe API key is unknown, malformed, or revoked.
HTTP429CodethrottledTypegateway_errorWhenThe 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.
HTTP403Codemissing_authentication_tokenTypegateway_errorWhenThe route does not exist and the request has no Authorization header. With a Bearer API key, the same request returns 403 gateway_error.
HTTPOtherCodegateway_errorTypegateway_errorWhenAny other gateway condition. Use the HTTP status and message.