https://txtfetch.com/docs/tables/
Do tables come back as tables?
Mostly. HTML, Word, Excel, PowerPoint and ODT tables come back as tables in Markdown and JSON. PDF and CSV tables do not. A merged cell in HTML or Word moves the cells after it.
The short answer
It depends on the format and on the output you ask for. The default text output has no table in it. Add format=markdown or format=json to get one.
HTML, Word, Excel, PowerPoint and ODT give a table in Markdown. It is a pipe table, except for a merged ODT cell. That table comes back as HTML with a colspan. In JSON, these formats give a table element with a cells grid.
PDF gives no table. Each row comes back as a line of text. CSV gives no table in Markdown or JSON. The whole file comes back as one line.
A merged cell is the hard case. Excel and PowerPoint keep every value in its column. ODT keeps the merge. HTML and Word lose it. The cells after the merge move one column left. In Markdown, that puts a number under the wrong header.
The engine is tika-server-standard 3.3.1.
Source: terraform/lambda-layers/build/versions.env (versions.env: TIKA_VERSION), accessed 2026-10-23.
What comes back, format by format
We sent seven file formats to the production API. Each file holds the same 3 by 3 table. In the merged test, the cell EMEA total spans the first two columns. The value 2.0 sits in the third column, under Q2. A second file for each format holds the same table with no merge.
| Format | Text | Markdown | JSON | Merged cell |
|---|---|---|---|---|
| HTML | One cell per line, with a leading tab. A blank line follows each row. | A pipe table. | A table element with a cells grid. | Lost. Every cell has colspan 1, and the cells after the merge move one column left. |
| Word | One cell per line, with a leading tab. A blank line follows each row. | A pipe table. | A table element with a cells grid. | Lost. Every cell has colspan 1, and the cells after the merge move one column left. |
| Excel | One tab-separated line per row, under the sheet name. | A sheet heading, then a pipe table. | A heading for the sheet, then a table element with a cells grid. | Kept in place. An empty cell holds each covered column. |
| PowerPoint | One tab-separated line per row. | A pipe table. | A table element with a cells grid. | Kept in place. An empty cell holds each covered column. |
| ODT | One cell per line, with a leading tab. A blank line follows each row. | A pipe table. A merged cell turns the table into an HTML table with colspan. | A table element with a cells grid. | Kept in cells and in format=markdown, as colspan 2. The markdown field inside the JSON table element moves the cells after it left. |
| CSV | One comma-separated line per row, as typed. | No table. The whole file is one line. | No table element. One paragraph holds the whole file. | A CSV file has no merged cells. |
| One line per row, with the values split by spaces. | No table. Each row is a paragraph. | No table element. Each row is a paragraph with a page number. | No table, so no merge. The row reads EMEA total 2.0. |
Source: marketing/test/fixtures/tables/README.md (Production API probe: POST /v1/extract), accessed 2026-10-23. The test PDF came from reportlab. A PDF from another tool may differ.
Real output
There are 39 samples, one for each file and output. Each block is the full response body, as the API sent it. In the JSON, \n is a line break and \t is a tab. Open a format to see its samples.
HTML
An HTML page with a merged cell
The merged cell comes back as one cell. The 2.0 is in the second cell of the row, so a reader cannot tell it belongs under Q2.
In
tables-merged.html
Out
{"status":"success","extracted_text":"\nSales by region.\n\n\n\n\tRegion\n\tQ1\n\tQ2\n\n\n\n\tNorth\n\t1.5\n\t2.5\n\n\n\n\tEMEA total\n\t2.0\n\n\n\n\n","metadata":{"content_type":"text/html","bytes":299,"chars":81,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An HTML page with a merged cell, as Markdown
Wrong data. The row is | EMEA total | 2.0 | |, so 2.0 sits under Q1 and Q2 is empty.
In
tables-merged.html (format=markdown)
Out
{"status":"success","markdown":"Sales by region.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| EMEA total | 2.0 | |\n","metadata":{"content_type":"text/html","bytes":299,"ocr":false,"chars":105,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An HTML page with a merged cell, as element JSON
Every cell has colspan 1. The third row has two cells for a three-column table. The html field has no colspan.
In
tables-merged.html (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by region.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nEMEA total\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| EMEA total | 2.0 | |","html":"<table><tbody><tr><th>Region</th><th>Q1</th><th>Q2</th></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>EMEA total</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":true},{"text":"Q1","colspan":1,"rowspan":1,"header":true},{"text":"Q2","colspan":1,"rowspan":1,"header":true}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"EMEA total","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":18,"bbox":null}],"metadata":{"content_type":"text/html","bytes":299,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An HTML page with no merged cell
One cell per line, each with a leading tab. A blank line follows each row. No row is on one line.
In
tables-plain.html
Out
{"status":"success","extracted_text":"\nSales by quarter.\n\n\n\n\tRegion\n\tQ1\n\tQ2\n\n\n\n\tNorth\n\t1.5\n\t2.5\n\n\n\n\tSouth\n\t1.0\n\t2.0\n\n\n\n\n","metadata":{"content_type":"text/html","bytes":296,"chars":82,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An HTML page with no merged cell, as Markdown
A Markdown pipe table.
In
tables-plain.html (format=markdown)
Out
{"status":"success","markdown":"Sales by quarter.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |\n","metadata":{"content_type":"text/html","bytes":296,"ocr":false,"chars":104,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An HTML page with no merged cell, as element JSON
A paragraph element, then a table element. It holds text, markdown, html and a cells array. Header cells are marked.
In
tables-plain.html (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by quarter.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nSouth\t1.0\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |","html":"<table><tbody><tr><th>Region</th><th>Q1</th><th>Q2</th></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>South</td><td>1.0</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":true},{"text":"Q1","colspan":1,"rowspan":1,"header":true},{"text":"Q2","colspan":1,"rowspan":1,"header":true}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"South","colspan":1,"rowspan":1,"header":false},{"text":"1.0","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":19,"bbox":null}],"metadata":{"content_type":"text/html","bytes":296,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
Word
A Word file with a merged cell
The merged cell comes back as one cell. The 2.0 follows it. Nothing shows that the cell spanned two columns.
In
tables-merged.docx
Out
{"status":"success","extracted_text":"Sales by region.\n\tRegion\n\tQ1\n\tQ2\n\n\tNorth\n\t1.5\n\t2.5\n\n\tEMEA total\n\t2.0\n\n\n\n\n\n\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","bytes":36797,"chars":75,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A Word file with a merged cell, as Markdown
Wrong data. The row is | EMEA total | 2.0 | |, so 2.0 sits under Q1 and Q2 is empty.
In
tables-merged.docx (format=markdown)
Out
{"status":"success","markdown":"Sales by region.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| EMEA total | 2.0 | |\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","bytes":36797,"ocr":false,"chars":105,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A Word file with a merged cell, as element JSON
Every cell has colspan 1. The third row has two cells for a three-column table. The html field has no colspan.
In
tables-merged.docx (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by region.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nEMEA total\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| EMEA total | 2.0 | |","html":"<table><tbody><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>EMEA total</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"EMEA total","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":18,"bbox":null}],"metadata":{"content_type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","bytes":36797,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A Word file with no merged cell
The same as HTML. One cell per line, each with a leading tab, and a blank line after each row.
In
tables-plain.docx
Out
{"status":"success","extracted_text":"Sales by quarter.\n\tRegion\n\tQ1\n\tQ2\n\n\tNorth\n\t1.5\n\t2.5\n\n\tSouth\n\t1.0\n\t2.0\n\n\n\n\n\n\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","bytes":36785,"chars":76,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A Word file with no merged cell, as Markdown
A Markdown pipe table.
In
tables-plain.docx (format=markdown)
Out
{"status":"success","markdown":"Sales by quarter.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","bytes":36785,"ocr":false,"chars":104,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A Word file with no merged cell, as element JSON
A paragraph element, then a table element with text, markdown, html and cells.
In
tables-plain.docx (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by quarter.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nSouth\t1.0\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |","html":"<table><tbody><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>South</td><td>1.0</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"South","colspan":1,"rowspan":1,"header":false},{"text":"1.0","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":19,"bbox":null}],"metadata":{"content_type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","bytes":36785,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
Excel
An Excel sheet with a merged cell
The merged cell keeps its place. An empty cell stands in for the covered one. 2.0 comes back as 2.
In
tables-merged.xlsx
Out
{"status":"success","extracted_text":"Sales\n\tSales by region.\n\tRegion\tQ1\tQ2\n\tNorth\t1.5\t2.5\n\tEMEA total\t\t2\n\n\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","bytes":4944,"chars":70,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An Excel sheet with a merged cell, as Markdown
Correct columns. The merged cell is followed by an empty cell, and 2 sits under Q2.
In
tables-merged.xlsx (format=markdown)
Out
{"status":"success","markdown":"# Sales\n\n| Sales by region. | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| EMEA total | | 2 |\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","bytes":4944,"ocr":false,"chars":121,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An Excel sheet with a merged cell, as element JSON
Every cell has colspan 1. An empty cell holds the covered column, so the third row has three cells.
In
tables-merged.xlsx (format=json)
Out
{"status":"success","elements":[{"type":"heading","text":"Sales","level":1,"page":null,"offset":0,"bbox":null},{"type":"table","text":"Sales by region.\nRegion\tQ1\tQ2\nNorth\t1.5\t2.5\nEMEA total\t\t2","markdown":"| | | |\n| --- | --- | --- |\n| Sales by region. | | |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| EMEA total | | 2 |","html":"<table><tbody><tr><td>Sales by region.</td></tr><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>EMEA total</td><td></td><td>2</td></tr></tbody></table>","cells":[[{"text":"Sales by region.","colspan":1,"rowspan":1,"header":false}],[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"EMEA total","colspan":1,"rowspan":1,"header":false},{"text":"","colspan":1,"rowspan":1,"header":false},{"text":"2","colspan":1,"rowspan":1,"header":false}]],"rows":4,"cols":3,"page":null,"offset":7,"bbox":null}],"metadata":{"content_type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","bytes":4944,"ocr":false,"chars":64,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An Excel sheet with no merged cell
The sheet name, then one tab-separated line per row. The sheet's own title cell is the first row. 1.0 comes back as 1.
In
tables-plain.xlsx
Out
{"status":"success","extracted_text":"Sales\n\tSales by quarter.\n\tRegion\tQ1\tQ2\n\tNorth\t1.5\t2.5\n\tSouth\t1\t2\n\n\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","bytes":4923,"chars":67,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An Excel sheet with no merged cell, as Markdown
A sheet heading, then a pipe table. The sheet's title cell is the first row. 1.0 comes back as 1.
In
tables-plain.xlsx (format=markdown)
Out
{"status":"success","markdown":"# Sales\n\n| Sales by quarter. | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| South | 1 | 2 |\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","bytes":4923,"ocr":false,"chars":118,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An Excel sheet with no merged cell, as element JSON
A heading element for the sheet, then a table element with text, markdown, html and cells.
In
tables-plain.xlsx (format=json)
Out
{"status":"success","elements":[{"type":"heading","text":"Sales","level":1,"page":null,"offset":0,"bbox":null},{"type":"table","text":"Sales by quarter.\nRegion\tQ1\tQ2\nNorth\t1.5\t2.5\nSouth\t1\t2","markdown":"| | | |\n| --- | --- | --- |\n| Sales by quarter. | | |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| South | 1 | 2 |","html":"<table><tbody><tr><td>Sales by quarter.</td></tr><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>South</td><td>1</td><td>2</td></tr></tbody></table>","cells":[[{"text":"Sales by quarter.","colspan":1,"rowspan":1,"header":false}],[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"South","colspan":1,"rowspan":1,"header":false},{"text":"1","colspan":1,"rowspan":1,"header":false},{"text":"2","colspan":1,"rowspan":1,"header":false}]],"rows":4,"cols":3,"page":null,"offset":7,"bbox":null}],"metadata":{"content_type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","bytes":4923,"ocr":false,"chars":61,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
PowerPoint
A PowerPoint slide with a merged cell
The merged cell keeps its place. An empty cell stands in for the covered one. 2.0 stays under Q2.
In
tables-merged.pptx
Out
{"status":"success","extracted_text":"Sales by region.\n\tRegion\tQ1\tQ2\n\tNorth\t1.5\t2.5\n\tEMEA total\t\t2.0\n\n\n\n\n\n\n\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.presentationml.presentation","bytes":28525,"chars":70,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PowerPoint slide with a merged cell, as Markdown
Correct columns. The merged cell is followed by an empty cell, and 2.0 sits under Q2.
In
tables-merged.pptx (format=markdown)
Out
{"status":"success","markdown":"Sales by region.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| EMEA total | | 2.0 |\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.presentationml.presentation","bytes":28525,"ocr":false,"chars":105,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PowerPoint slide with a merged cell, as element JSON
Every cell has colspan 1. An empty cell holds the covered column, so the third row has three cells.
In
tables-merged.pptx (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by region.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nEMEA total\t\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| EMEA total | | 2.0 |","html":"<table><tbody><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>EMEA total</td><td></td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"EMEA total","colspan":1,"rowspan":1,"header":false},{"text":"","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":18,"bbox":null}],"metadata":{"content_type":"application/vnd.openxmlformats-officedocument.presentationml.presentation","bytes":28525,"ocr":false,"chars":60,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PowerPoint slide with no merged cell
One tab-separated line per row, each with a leading tab.
In
tables-plain.pptx
Out
{"status":"success","extracted_text":"Sales by quarter.\n\tRegion\tQ1\tQ2\n\tNorth\t1.5\t2.5\n\tSouth\t1.0\t2.0\n\n\n\n\n\n\n\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.presentationml.presentation","bytes":28502,"chars":69,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PowerPoint slide with no merged cell, as Markdown
A Markdown pipe table.
In
tables-plain.pptx (format=markdown)
Out
{"status":"success","markdown":"Sales by quarter.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |\n","metadata":{"content_type":"application/vnd.openxmlformats-officedocument.presentationml.presentation","bytes":28502,"ocr":false,"chars":104,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PowerPoint slide with no merged cell, as element JSON
A paragraph element, then a table element with text, markdown, html and cells.
In
tables-plain.pptx (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by quarter.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nSouth\t1.0\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |","html":"<table><tbody><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>South</td><td>1.0</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"South","colspan":1,"rowspan":1,"header":false},{"text":"1.0","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":19,"bbox":null}],"metadata":{"content_type":"application/vnd.openxmlformats-officedocument.presentationml.presentation","bytes":28502,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
ODT
An ODT file with a merged cell
The merged cell comes back as one cell. The 2.0 follows it. Nothing shows that the cell spanned two columns.
In
tables-merged.odt
Out
{"status":"success","extracted_text":"Sales by region.\n\tRegion\n\tQ1\n\tQ2\n\n\tNorth\n\t1.5\n\t2.5\n\n\tEMEA total\n\t2.0\n\n\n","metadata":{"content_type":"application/vnd.oasis.opendocument.text","bytes":1669,"chars":71,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An ODT file with a merged cell, as Markdown
The merge survives. The table comes back as an HTML table with colspan="2" inside the Markdown.
In
tables-merged.odt (format=markdown)
Out
{"status":"success","markdown":"Sales by region.\n\n<table>\n<tbody>\n<tr><td>Region</td><td>Q1</td><td>Q2</td></tr>\n<tr><td>North</td><td>1.5</td><td>2.5</td></tr>\n<tr><td colspan=\"2\">EMEA total</td><td>2.0</td></tr>\n</tbody>\n</table>\n","metadata":{"content_type":"application/vnd.oasis.opendocument.text","bytes":1669,"ocr":false,"chars":200,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An ODT file with a merged cell, as element JSON
The merge survives in cells and in html, as colspan 2. The markdown field of the element does not keep it. Its row 3 is | EMEA total | 2.0 | |, so 2.0 moves under Q1.
In
tables-merged.odt (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by region.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nEMEA total\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| EMEA total | 2.0 | |","html":"<table><tbody><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td colspan=\"2\">EMEA total</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"EMEA total","colspan":2,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":18,"bbox":null}],"metadata":{"content_type":"application/vnd.oasis.opendocument.text","bytes":1669,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An ODT file with no merged cell
The same as Word. One cell per line, each with a leading tab, and a blank line after each row.
In
tables-plain.odt
Out
{"status":"success","extracted_text":"Sales by quarter.\n\tRegion\n\tQ1\n\tQ2\n\n\tNorth\n\t1.5\n\t2.5\n\n\tSouth\n\t1.0\n\t2.0\n\n\n","metadata":{"content_type":"application/vnd.oasis.opendocument.text","bytes":1569,"chars":72,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An ODT file with no merged cell, as Markdown
A Markdown pipe table.
In
tables-plain.odt (format=markdown)
Out
{"status":"success","markdown":"Sales by quarter.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |\n","metadata":{"content_type":"application/vnd.oasis.opendocument.text","bytes":1569,"ocr":false,"chars":104,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
An ODT file with no merged cell, as element JSON
A paragraph element, then a table element with text, markdown, html and cells.
In
tables-plain.odt (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Sales by quarter.","page":null,"offset":0,"bbox":null},{"type":"table","text":"Region\tQ1\tQ2\nNorth\t1.5\t2.5\nSouth\t1.0\t2.0","markdown":"| | | |\n| --- | --- | --- |\n| Region | Q1 | Q2 |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |","html":"<table><tbody><tr><td>Region</td><td>Q1</td><td>Q2</td></tr><tr><td>North</td><td>1.5</td><td>2.5</td></tr><tr><td>South</td><td>1.0</td><td>2.0</td></tr></tbody></table>","cells":[[{"text":"Region","colspan":1,"rowspan":1,"header":false},{"text":"Q1","colspan":1,"rowspan":1,"header":false},{"text":"Q2","colspan":1,"rowspan":1,"header":false}],[{"text":"North","colspan":1,"rowspan":1,"header":false},{"text":"1.5","colspan":1,"rowspan":1,"header":false},{"text":"2.5","colspan":1,"rowspan":1,"header":false}],[{"text":"South","colspan":1,"rowspan":1,"header":false},{"text":"1.0","colspan":1,"rowspan":1,"header":false},{"text":"2.0","colspan":1,"rowspan":1,"header":false}]],"rows":3,"cols":3,"page":null,"offset":19,"bbox":null}],"metadata":{"content_type":"application/vnd.oasis.opendocument.text","bytes":1569,"ocr":false,"chars":59,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
CSV
A CSV file with no merged cell
The file as typed. One comma-separated line per row.
In
tables-plain.csv
Out
{"status":"success","extracted_text":"Region,Q1,Q2\nNorth,1.5,2.5\nSouth,1.0,2.0\n\n","metadata":{"content_type":"text/plain","bytes":41,"chars":42,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A CSV file with no merged cell, as Markdown
No table. The whole file is one line, and the row breaks are now spaces.
In
tables-plain.csv (format=markdown)
Out
{"status":"success","markdown":"Region,Q1,Q2 North,1.5,2.5 South,1.0,2.0\n","metadata":{"content_type":"text/plain","bytes":41,"ocr":false,"chars":41,"format":"markdown","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A CSV file with no merged cell, as element JSON
One paragraph element. No table element. The row breaks are now spaces.
In
tables-plain.csv (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"Region,Q1,Q2 North,1.5,2.5 South,1.0,2.0","page":null,"offset":0,"bbox":null}],"metadata":{"content_type":"text/plain","bytes":41,"ocr":false,"chars":40,"format":"json","tier":"standard","pages":null,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PDF with a merged cell
No table. The row is the line EMEA total 2.0.
In
tables-merged.pdf
Out
{"status":"success","extracted_text":"\nSales by region.\n\nRegion Q1 Q2\n\nNorth 1.5 2.5\n\nEMEA total 2.0\n\n\n","metadata":{"content_type":"application/pdf","bytes":1621,"chars":65,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PDF with a merged cell, as Markdown
No table. The row is the line EMEA total 2.0.
In
tables-merged.pdf (format=markdown)
Out
{"status":"success","markdown":"Sales by region.\n\nRegion Q1 Q2\n\nNorth 1.5 2.5\n\nEMEA total 2.0\n","metadata":{"content_type":"application/pdf","bytes":1621,"ocr":false,"chars":62,"format":"markdown","tier":"standard","pages":1,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PDF with a merged cell, as element JSON
No table element. The row is the paragraph EMEA total 2.0.
In
tables-merged.pdf (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"","page":1,"offset":0,"bbox":null},{"type":"paragraph","text":"Sales by region.","page":1,"offset":0,"bbox":null},{"type":"paragraph","text":"Region Q1 Q2","page":1,"offset":18,"bbox":null},{"type":"paragraph","text":"North 1.5 2.5","page":1,"offset":32,"bbox":null},{"type":"paragraph","text":"EMEA total 2.0","page":1,"offset":47,"bbox":null},{"type":"paragraph","text":"","page":1,"offset":63,"bbox":null}],"metadata":{"content_type":"application/pdf","bytes":1621,"ocr":false,"chars":63,"format":"json","tier":"standard","pages":1,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PDF with no merged cell
One line per row, with the values split by spaces. No tab, no table.
In
tables-plain.pdf
Out
{"status":"success","extracted_text":"\nSales by quarter.\n\nRegion Q1 Q2\n\nNorth 1.5 2.5\n\nSouth 1.0 2.0\n\n\n","metadata":{"content_type":"application/pdf","bytes":1624,"chars":65,"ocr":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PDF with no merged cell, as Markdown
No table. Each row is a plain paragraph of space-separated values.
In
tables-plain.pdf (format=markdown)
Out
{"status":"success","markdown":"Sales by quarter.\n\nRegion Q1 Q2\n\nNorth 1.5 2.5\n\nSouth 1.0 2.0\n","metadata":{"content_type":"application/pdf","bytes":1624,"ocr":false,"chars":62,"format":"markdown","tier":"standard","pages":1,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
A PDF with no merged cell, as element JSON
Paragraph elements only. No table element. Each row is its own paragraph, with a page number.
In
tables-plain.pdf (format=json)
Out
{"status":"success","elements":[{"type":"paragraph","text":"","page":1,"offset":0,"bbox":null},{"type":"paragraph","text":"Sales by quarter.","page":1,"offset":0,"bbox":null},{"type":"paragraph","text":"Region Q1 Q2","page":1,"offset":19,"bbox":null},{"type":"paragraph","text":"North 1.5 2.5","page":1,"offset":33,"bbox":null},{"type":"paragraph","text":"South 1.0 2.0","page":1,"offset":48,"bbox":null},{"type":"paragraph","text":"","page":1,"offset":63,"bbox":null}],"metadata":{"content_type":"application/pdf","bytes":1624,"ocr":false,"chars":63,"format":"json","tier":"standard","pages":1,"vlm":false}}Source: Production API probe: POST /v1/extract, accessed 2026-10-23.
What to do
For HTML, Word, Excel, PowerPoint and ODT, ask for format=markdown. Chunk by table. Keep the header row with every chunk. The tables guide shows how.
Use format=json when you need the cells. Each table element has a cells grid. Each cell holds its text, its colspan and its rowspan. The markdown field inside a table element starts with an empty header row. It also has no merged cells. For a merged ODT cell, the field moves the cells after it one column left, but cells keeps colspan 2. Build your own Markdown from cells, or use format=markdown.
Check the merged cells. For HTML and Word, the JSON has no colspan. A row with a merge has fewer cells than the header row. The snippet below finds those rows. If a row is short, do not trust its column order. Read the source file for that table.
For a PDF table, there is no structure on the standard tier. You get the cell text, row by row. Read the table with a tool that reads the page layout, and send the result to your index. See Tables losing structure.
For CSV, use the default text output. It keeps one row on each line. The Markdown and JSON outputs join the rows into one line.
Find a table that lost a merged cell
Add up the colspan of each row. In a good table every row covers the same number of columns. A short row means a merge was lost.
Python, run on tables-merged-html-json.json
import json
with open("tables-merged-html-json.json", encoding="utf-8") as f:
elements = json.load(f)["elements"]
for element in elements:
if element["type"] != "table":
continue
# Each row should cover the same number of columns.
widths = [sum(cell["colspan"] for cell in row) for row in element["cells"]]
for number, width in enumerate(widths, start=1):
if width != max(widths):
print(f"row {number} covers {width} of {max(widths)} columns")It printed
row 3 covers 2 of 3 columns
It printed the same line for the merged Word file. It printed nothing for the merged PowerPoint and ODT files. It printed nothing for the HTML, Word, PowerPoint and ODT files with no merged cell.
Excel gives a false alarm. The snippet printed row 1 covers 1 of 3 columns for both Excel files. The title cell of the sheet comes back as a first row with one cell. That row is not a lost merge. Ignore a one-cell row at the top of an Excel table.
Web pages and the premium tier
We tested uploaded files. We did not test a page that txtfetch fetches from a URL. We tested one small table for each format. A large table, a nested table or a table that spans pages may behave in other ways. We did not measure tables on the premium tier. We make no claim about them.
?quality=premium is sold as a tier, but it does not run today. VLM_ENABLED defaults to false, and no operator has switched the tier on in production. A premium request downgrades to the standard Tika path instead.
Read Output modes for the three response shapes. Read the PDF page, the Excel page and the Office page for the rest of each format.
Frequently asked questions
- Do tables come back as tables?
- For HTML, Word, Excel, PowerPoint and ODT, yes, if you ask for format=markdown or format=json. The default text output has no table. For PDF and CSV, no. A PDF table comes back as lines of text. A CSV file comes back as one line in Markdown and JSON.
- What happens to a merged cell?
- It depends on the format. Excel and PowerPoint keep every value in its column. ODT keeps the merge as colspan. HTML and Word lose the colspan, and the cells after the merge move one column left. In Markdown, that puts a number under the wrong header.
- Does a PDF table come back as a table?
- No. In our test each row came back as one line of space-separated values. The Markdown output had no pipe table. The JSON output had no table element. Read the PDF table with a tool that reads the page layout.
- Is the cells grid in format=json safe to trust?
- Check it first. The grid holds the cells in order, but for HTML and Word it holds no colspan. A row with a merged cell has fewer cells than the header row. The snippet on this page finds those rows.
- Why does my Excel 1.0 come back as 1?
- In our test the cells held numbers in the General format. A cell that held 2.0 came back as 2, and 1.0 came back as 1. The other formats kept 2.0 as typed. Check any column where a trailing zero matters.
- Which output should I use for RAG?
- Use format=markdown for HTML, Word, Excel, PowerPoint and ODT. Chunk by table, and keep the header row with each chunk. Use format=json when you need the cells. For a PDF, read the table with a layout tool and send the result to your index.
- Does the premium tier read tables better?
- We did not measure tables on the premium tier. We make no claim about it. The premium tier does not run today, so a premium request downgrades to the standard Tika path.
Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
Open the playground →