OCR API for PDFs and images.
Post a document to one endpoint and get its text back, page by page. No upload UI, no SDK, no monthly bill — just a Bearer key and a balance of pages.
From 1.0¢ a page · Pages never expire
PDFs, scans and photos
PDF, JPEG, PNG, TIFF, BMP and HEIF up to 50MB, multi-page or single shot.
One request, all the text
Get the whole document as a string and the same text split page by page.
Prepaid, never a subscription
Buy pages once. They never expire and nothing renews on your card.
Pricing
One page of a document costs one page of your balance. Buy a pack, spend it whenever, buy another when it runs out.
One-time purchase, no recurring charge. Payments are handled securely by Paddle.
API reference
The whole API is two endpoints. Here is all of it.
Overview
One endpoint takes a PDF or an image and returns its text, both as a single string and split page by page. There is no upload UI and no SDK to install: everything below is a plain HTTP request you can make from any language.
| Base URL | https://404tools.dev |
|---|---|
| Read a document | POST /api/v1/ocr |
| Check your balance | GET /api/v1/account |
| Model | prebuilt-read |
Authentication
Every request carries an API key as a Bearer token. Keys start with tk_live_ and are shown in full exactly once, when you create them. Create a key and store it the way you store any other secret.
Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKeys are stored only as a SHA-256 digest, so a lost key cannot be recovered — revoke it and issue another. Cross-origin requests are allowed because the credential is a header you attach deliberately rather than a cookie the browser attaches for you, but that is not an invitation to ship a key to a browser: anything in front of a user should call your own backend, which then calls this API.
Sending a document
Three request shapes, all of them POST /api/v1/ocr. Pick whichever fits the language you are calling from; they produce identical responses. The content type you declare is only a hint — the bytes themselves decide how the document is treated, so a file that arrives as application/octet-stream still works.
Send multipart/form-data with the document in a field named file. This is the shape almost every HTTP client makes easiest.
curl -X POST https://404tools.dev/api/v1/ocr \
-H "Authorization: Bearer $TOOLS_API_KEY" \
-F "file=@invoice.pdf"Documents it accepts
| Content type | Notes |
|---|---|
| application/pdf | PDF, including multi-page scans |
| image/jpeg | JPEG photos and scans |
| image/png | PNG screenshots and exports |
| image/tiff | TIFF, including multi-page faxes |
| image/bmp | Windows bitmap |
| image/heif | HEIF/HEIC from modern phones |
Common aliases are normalised for you, so image/jpg and image/heic are fine. Anything else is refused with a 415 before the bytes leave our network. The hard ceiling is 50MB per document, checked both against the declared length and against the bytes actually read.
The response
A successful call returns 200 and the JSON below. content is the whole document as one string with page breaks preserved as newlines; pages is the same text split into one entry per page, which is what you want if you are indexing or chunking.
{
"content": "INVOICE\nAcme Corp\nInvoice #1042\n...",
"pageCount": 3,
"pages": [
{ "pageNumber": 1, "text": "INVOICE\nAcme Corp\n..." },
{ "pageNumber": 2, "text": "Line items\n..." },
{ "pageNumber": 3, "text": "Total due\n..." }
],
"pagesCharged": 3,
"pagesRemaining": 2497,
"durationMs": 4120
}| content | Full extracted text, all pages. |
|---|---|
| pageCount | Pages the document actually had. |
| pages[] | pageNumber (1-based) and text for each page. |
| pagesCharged | Pages debited from your balance for this call. |
| pagesRemaining | Balance left after the debit. |
| durationMs | Time the OCR itself took, not your upload. |
How pages are counted and billed
Billing is prepaid pages, one page per page read. A ten-page PDF costs ten pages; a single photo costs one. Pages are bought in packs, never expire, and there is no subscription — when a balance runs out you buy another pack.
Only successful reads are charged. A 401, 413, 415, 429 or 502 costs you nothing, and neither does a 402. A document is billed whole or not at all: if it has more pages than your balance the call returns 402 with the pageCount it would have cost and your pagesRemaining, nothing is debited, and no text is returned. Buy a pack and send it again.
To check a balance without spending a page, call the account endpoint. Doing that before a batch job is the cheapest way to avoid discovering an empty balance halfway through one.
curl https://404tools.dev/api/v1/account \
-H "Authorization: Bearer $TOOLS_API_KEY"
{ "pagesBalance": 2497, "pagesUsedTotal": 503 }Rate limits
Each API key may make 20 requests per minute. Going over returns 429 with a Retry-After header in seconds; wait that long rather than retrying immediately. The limit is per key, so splitting a workload across several keys raises your effective throughput. Long documents are the other limit worth knowing about: a request may run for up to five minutes, so set your client timeout accordingly rather than leaving it at the usual 30 seconds.
Errors
Every failure returns JSON with a human-readable error and a stable code. Branch on the code, not on the message text.
{
"error": "This document is 12 page(s) and your balance is 4. Nothing was charged.",
"code": "insufficient_pages",
"pageCount": 12,
"pagesRemaining": 4
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_body | The body could not be parsed as the Content-Type promised. |
| 400 | missing_file | A multipart request arrived without a "file" field. |
| 400 | missing_url | A JSON request arrived without a "url" string. |
| 400 | invalid_url | The url was unparseable or used a scheme other than http/https. |
| 400 | url_fetch_failed | The document could not be downloaded from the url you gave. |
| 400 | empty_document | The request carried no bytes, or the url returned an empty body. |
| 401 | invalid_api_key | Missing, malformed, unknown or revoked key. All four look the same on purpose. |
| 402 | insufficient_pages | Your balance is empty, or smaller than this document. Buy a page pack and retry; nothing was charged. |
| 413 | document_too_large | The document is over the 50MB limit. |
| 415 | unsupported_media_type | The bytes are not one of the supported document types. |
| 429 | rate_limited | More than 20 requests in a minute on this key. Honour the Retry-After header. |
| 500 | accounting_failed | Page accounting failed, so the text is withheld. Nothing was charged. |
| 502 | ocr_failed | The upstream OCR engine rejected or failed on the document. Nothing was charged. |
Copy-paste examples
All three read a local PDF and print its text. Keep the key in an environment variable — the examples assume TOOLS_API_KEY.
curl -X POST https://404tools.dev/api/v1/ocr \
-H "Authorization: Bearer $TOOLS_API_KEY" \
-F "file=@invoice.pdf"Create an account, buy a pack, start reading documents.
You get your first key the moment your pack clears. Nothing renews and nothing expires.