Extract tables from PDF text
Text whose values line up by tabs, such as Cable, 25 and 1.20, becomes a table with three named columns and three rows of data. Prose without aligned columns is refused rather than forced into a fake table. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
tab-aligned columns to JSON rows
From raw PDF text to a structured table
When you run a text extractor over a PDF page, tables come back as flat lines of text. A human can still see the columns because the values line up — separated by tab characters or by wide gaps of two or more spaces — but a program cannot. This capability closes that gap.
You send the page's raw lines and it finds the aligned columns: every line is split on the detected separator, the column count that repeats across lines is taken as the table's shape, and each line is cut into cells. The first row becomes the header by default, so the output is a named columns array plus a rows array of data, ready to map into objects or load into a dataframe.
Everything is deterministic — the same text always yields the same table — which makes it safe to put in an automated pipeline where an OCR or vision model would introduce silent variation.
How the parsing actually works
First, blank lines are dropped and each remaining line is split into cells. In auto mode the parser uses tab characters when any line contains one, and otherwise falls back to runs of two or more spaces — you can also force either strategy with the delimiter parameter. Only lines that yield at least two cells are candidates for the table.
Among those, the parser looks for the column count that repeats: the mode of the distribution. A real table is lines that agree on how many columns they have, so lines with a different count — a wrapped paragraph, a stray footer — are set aside. If no column count repeats at least twice, or fewer than two candidate lines exist, the input is rejected: there is no table to find.
Disable it for headerless data and the synthetic names apply to every row instead.
Where it fits and what it is not
Be clear about what it is not. It does not read the PDF file itself — it works on text you have already extracted. It does not handle scanned pages, because those have no text layer; for images of tables you want an OCR-based capability.
And it expects aligned columns, not prose: free-flowing text is rejected rather than mangled into a fake table.
What you can do with it
Automate a report-ingestion pipeline
Run the parser after your text extractor on every page and load only the pages that parse into tables — the rest are rejected cleanly and not billed.
FAQ
Does it read the PDF file directly?
No. It parses the raw text layer of one page that you supply. Extract the text first with a PDF text extractor, then send the lines here.
What column separators are supported?
Tab characters, or runs of two or more spaces. The auto mode detects which one the page uses; you can also force tab or spaces with the delimiter parameter.
What happens when the text has no table?
Invalid calls are rejected before billing, so they cost nothing.
Does it work on scanned PDFs?
No. A scan is an image and has no text layer to parse. Use an OCR table extractor for scanned documents; this capability is for digital PDFs with selectable text.
Is my document stored?
No. The text is processed in memory and discarded; only the parsed table is returned.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/pdf/extract-tables \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Item\tQty\tUnit price\nWidget\t12\t9.99\nGadget\t3\t24.50"}'const res = await fetch("https://api.kit.forhosting.com/pdf/extract-tables", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"text": "Item\tQty\tUnit price\nWidget\t12\t9.99\nGadget\t3\t24.50"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/pdf/extract-tables",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"text": "Item\tQty\tUnit price\nWidget\t12\t9.99\nGadget\t3\t24.50"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/pdf/extract-tables", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"text":"Item\\tQty\\tUnit price\\nWidget\\t12\\t9.99\\nGadget\\t3\\t24.50"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"text":"Item\tQty\tUnit price\nWidget\t12\t9.99\nGadget\t3\t24.50"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/pdf/extract-tables", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"text": "Item\tQty\tUnit price\nWidget\t12\t9.99\nGadget\t3\t24.50"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "pdf.extract_tables",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 1 |
max_lines | 5000 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
The extract tables from PDF text API takes the raw text layer of a PDF page — the lines you get from a text extractor, where columns are aligned with tabs or with runs of spaces — and parses it into a clean, structured table: column names, data rows, and counts, as JSON you can feed straight into a spreadsheet, a database import or a script. It is the deterministic step between extracting text from a PDF and actually using the numbers inside it, with no OCR and no AI guessing involved.
From raw PDF text to a structured table
If the page contains no aligned columns at all, the call fails with a clear invalid_input error instead of inventing a table, and failed calls are not billed.
How the parsing actually works
With has_header enabled, the first table row names the columns; empty or duplicated header cells are replaced with col_1, col_2 and so on, so every output column name is usable.
Where it fits and what it is not
Use it as the second stage of a PDF data pipeline: extract the text layer with a text extractor, then call this endpoint on each page that holds a table, and load the resulting rows wherever they belong. Because the parse is pure text processing, it runs at the edge in milliseconds, costs $0.002 per call, and the identical code runs free in your browser on this page, so you can test a page's text by pasting it before you automate anything. It is equally useful for one-off jobs: paste the text from a report page, get JSON, convert to CSV in one more step.
Integration use cases
Recover a price list from a PDF page
Extract the page text, send it in, and get item, quantity and price columns as JSON rows ready for a spreadsheet import.
Convert a headerless data dump
Turn off header detection with has_header=false and get stable col_1..col_n names for every row of a space-aligned listing.
Technical questions
What does it cost?
$0.002 per call. The same parser also runs free in your browser on this page, so you only pay when you automate it through the API.