Guide
Text-layer PDFs and scanned PDFs
People say "PDF" as if it were one format. For table extraction there are two. A text-layer PDF has characters the computer can select. A scanned PDF is a picture of a page, sometimes with a hidden OCR layer, often without one. TableJSON only reads the first kind. It does not look at pixels, and it will not guess a digit from a smudge. Knowing which file you hold saves you a wasted upload.
The highlight test
Open the PDF in Chrome, Preview, or Acrobat. Click and drag across a price. If the digits turn into a selection, like text in a browser, the file has a text layer and it is worth uploading. If the drag draws a box over a photo and nothing selects, it is an image. Zoom to 300 percent. Text-layer type stays sharp. A scan goes soft and you can see the halftone or the shadow of the paper.
A second check, if you like a terminal: pdftotext -layout invoice.pdf -. Text on the screen means a text layer. An empty result, or a warning and no words, means TableJSON will also see nothing useful. You do not need that command for day-to-day use. The highlight test is enough.
There is a sneaky third case. Some scanners and some "print to PDF" drivers run OCR and then hide the recognized text under the image, or they flatten everything and throw the text away. Highlight still tells the truth. If you can select the words, we can read them, even when a photo of the page is painted on top. If you cannot, the OCR either failed or was discarded.
What TableJSON returns for each
A text-layer table comes back as JSON with headers, rows, page_number, and page_end. metadata.extraction_mode is always text. That field is not a detector you can flip. It is a reminder of the only mode this service runs. The walkthrough of a normal extract is in extracting tables from a PDF into JSON.
A scanned invoice on the invoice card returns a workbook anyway. The checksum cell reads FAIL: no text layer. The line table is empty. That FAIL is the product working. A green total on a file with no characters would be a fabrication. A scanned file on the left card, the general extract, parses if it is a valid PDF and then finds no columns. You get tables_found: 0 when there is no text at all. If the scan includes a header and footer that happen to be real text, those lines may show up and the body of the table will not. Do not treat a stray page number as a recovered ledger.
A corrupt file, or a file that is not a PDF despite the name, comes back as INVALID_PDF with a short reason. Renaming a JPEG to .pdf is in that bucket. The extractor does not convert images.
How good PDFs get turned into scans
The usual story is a bank, a carrier, or a government portal. You click Download PDF and the file looks perfect, because it was printed from their system. Highlight works, TableJSON works. The next month someone in the branch scans the same statement on a multifunction copier and emails that. It looks identical at a glance and extracts as nothing.
Other ways the text layer dies:
- Opening a digital PDF and "printing" it to a new PDF through a driver that rasterizes.
- Using a mobile scanner app and emailing the result as PDF without an OCR option enabled.
- Exporting from a design tool where the table was outlined shapes, not a text frame. The page looks like a table. Selection finds nothing, or finds one text box for the whole title.
- A security flatten that burns the page to an image to stop copying. The publisher got what they wanted. You will not get cells.
If you created the PDF yourself from Excel, Word, or Google Sheets, export again and choose the application's PDF export, not a scanner. Sheets and Excel can also give you CSV directly, which you should prefer. TableJSON is the fallback for a file someone else already froze.
What to do when you only have the scan
A short bill, five lines, is faster to type than to push through an OCR pipeline and then clean. A 12-page statement is not. Run OCR in a tool that writes a real text layer back into a PDF, then upload that PDF here. The order matters. OCR first, TableJSON second. This site will not call out to a vision model on your behalf, and the privacy page would be a different document if it did.
After OCR, do the highlight test again before you celebrate. Cheap OCR on a creased page swaps 8 and 6, drops a decimal, or glues two columns into one word. TableJSON will faithfully return that mistake. The checksum on an invoice is useful here: if OCR changed a unit price, line_check goes FAIL and the remainder shows the gap. A statement has no checksum in this app. Reconcile the opening balance, the sum of the amounts, and the closing balance yourself, as the bank statement guide describes.
Watch column spacing after OCR. Some engines put a single space between cells. TableJSON splits on two or more spaces, or on tabs, pipes, or commas. A single space does not count, and the row collapses to one cell. If the highlight works but the JSON is one column, the text layer is real and the spacing is wrong. Re-OCR with a layout mode, or export to CSV from the OCR tool, or insert the gaps. The cleaning guide covers the one-column case once you already have JSON.
A habit that avoids the argument
Before you send a PDF to a colleague or to a job, try to select one number. Mention in the email whether the text layer is there. "Digital PDF, text selectable" and "scan, needs OCR" are different tasks, and they should not share a folder. On TableJSON, if Extract finishes in a few dozen milliseconds with zero tables, assume the layer is missing until you prove otherwise. Speed plus emptiness is the signature of a picture. A real table of any size takes the parser a moment and returns rows you can find on the page.
If you are automating, branch on that result. tables_found === 0, or an invoice checksum of FAIL: no text layer, goes to a human queue or to your OCR step. It should not go into the ledger import. The API guide shows the status fields to switch on. Retrying the same scan will not grow a text layer.