Extract tables to CSV
The Table Extraction API looks at an image or PDF, finds the tables on the page, and returns their contents as CSV with rows and columns preserved instead of a wall of unstructured text. It exists for the specific frustration of tabular data trapped in a document, where regular OCR flattens a grid into a jumble of words in the wrong order.
Run it online
Run this on our servers with your account. Free tools run in your browser; this one bills your KIT balance per the price above.
Why tables need their own approach
Plain text OCR reads left to right, top to bottom, which works fine for prose but breaks a table apart: a row's values get separated by everything above and below them, columns bleed into each other, and what was a clean grid on paper becomes a scramble of numbers with no way to tell which row or column they belonged to. Financial statements, price lists, lab results, and shipping manifests are built entirely around that grid structure, so losing it defeats the point of extracting the data at all — which is the exact problem the table extraction api is built to avoid.
How the extraction works
You submit an image or PDF containing one or more tables, and the service detects the table's structure — where rows start and end, how many columns there are, which cells align with which header — before extracting the content and returning it as CSV, ready to open in a spreadsheet or load straight into a database. Because it identifies structure first, merged cells, multi-line entries within a cell, and tables with uneven column widths are handled the way a person reading the page would interpret them, not just as raw text positions.
The asynchronous flow
Calling POST /ocr/table returns a task_id immediately, and extraction runs on our global edge without holding a connection open while the document is processed. Once finished, results arrive via a signed webhook — the fit for a data pipeline ingesting tables continuously — or through a signed link valid 24 hours, when someone just needs one CSV out of one document.
Where structured extraction earns its keep
This is for the moment a business process depends on numbers that currently live only inside a PDF or scanned page: a finance team pulling figures from a supplier's price list PDF, an analyst converting a scanned lab report's results table into a spreadsheet, a logistics operation extracting line items from a shipping manifest. Since every call is a single async request returning CSV, feeding the output into existing spreadsheet tools or a database import script requires no extra parsing step.
Pricing and what happens when extraction fails
The cost is $0.010 per request plus $0.0575 per page, a published, scale-with-length rate rather than a flat fee that punishes short documents or a hidden one that surprises on long ones. A page where table structure can't be reliably detected is retried three times before a clear error is returned, and failed tasks are never billed, so you pay only for tables actually extracted. Access requires prepaid balance, with no free tier or trial, keeping the service fast and abuse-free; requests without balance return 402. Source files and extracted CSV are deleted after the retention window and never used for training.
What you can do with it
Supplier price list ingestion
A purchasing team converts a supplier's PDF price list into CSV and imports it directly into their pricing database instead of retyping every row.
Lab result digitization
A clinical analyst extracts the results table from a scanned lab report into a spreadsheet, preserving which value belongs to which test.
Shipping manifest processing
A logistics operation pulls line items from photographed shipping manifests into CSV, feeding warehouse and customs systems without manual re-entry.
Financial statement analysis
An analyst extracts a table of figures from a scanned financial statement and loads it straight into a spreadsheet for comparison against prior periods.
FAQ
How do I extract a table from a PDF or image into CSV?
Send the file to POST /ocr/table and you get a task_id right away. The table extraction api detects rows and columns asynchronously and delivers CSV by signed webhook or a signed link valid 24 hours.
Is the table extraction API free?
There's no free tier — free tiers get abused and slow everyone down. Access runs on a prepaid ForHosting KIT balance: top up from $10.00 (it never expires) and each request is charged at its published price, so a call with no balance returns HTTP 402. No subscription, no tokens, no invented credits, and a failed task is never charged.
What format is the extracted table returned in?
CSV, with rows and columns preserved as they appear in the source table, ready to open in a spreadsheet or import into a database.
Does it work on scanned images as well as PDFs?
Yes, both image files and PDF pages are supported, since the service detects table structure visually rather than relying on an existing text layer.
Can it handle merged cells or multi-line entries in a table?
Yes, the service detects table structure before extracting content, so merged cells and cells with wrapped text are interpreted the way a reader would understand them.
How is this different from regular OCR on a table?
Regular OCR reads text in reading order and can scramble a table's rows and columns; this endpoint detects the grid first, so the output preserves which value belongs to which row and column.
Can I extract tables from multiple documents in bulk?
Yes. Each request is an independent async call with its own task_id, so a batch of price lists or reports can be submitted together and results collected as CSV as they finish.
What happens if a table can't be detected reliably?
The system retries three times before returning a clear error, and a failed task is never charged, so cost only reflects tables actually extracted.
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, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/ocr/table \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"…"}'const res = await fetch("https://api.kit.forhosting.com/ocr/table", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"input": "…"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/ocr/table",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"input": "…"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/ocr/table", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"input":"…"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"input":"…"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/ocr/table", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"input": "…"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "ocr.table",
"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 | 25 |
max_pages | 10 |
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. |
413 | input_too_large | The file exceeds the size limit. |
422 | task_failed | The task failed after 3 retries. You are never charged for it. |