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.

What seven formats return today
FormatTextMarkdownJSONMerged cell
HTMLOne 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.
WordOne 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.
ExcelOne 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.
PowerPointOne 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.
ODTOne 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.
CSVOne 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.
PDFOne 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.

PDF

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 →

Get an API key →