Does AWS Textract Output CSV? What the API Actually Returns, and How to Build the File

Aug 14, 2026 11 min read

Textract detects tables well and hands you none of them as a spreadsheet. It returns a graph of Block objects, and AWS answers the CSV question with a code sample. Here is what the response really contains, the one documented behavior that quietly ruins most exports, and what the TABLES feature costs against plain text.

// Try it now, no signup required

PDF, JPG, PNG, BMP, HEIC, TIFF

Upload a document to extract

Free on your own files. No credit card, no signup to test.

The short answer: no, AWS Textract does not output CSV. There is no CSV, XLSX or spreadsheet output format in any Textract operation. Tables come back as Block objects inside the JSON response: a TABLE block linked to one CELL block per cell, each carrying a row index, a column index and a confidence value. If you want a CSV file, you write the writer. Amazon's own documentation says so in the plainest way possible, by answering the question with a link to a code sample titled "Exporting Tables into a CSV File".

That is not a criticism of the service. Textract's table detection is genuinely good, and a graph of blocks with geometry and confidence on every node carries far more information than a flat file could. But it does mean that "convert PDF to CSV with Textract" is a two-part job, and the second part is where teams lose a week. This article walks the response shape, then covers the one documented behavior that silently corrupts most first attempts.

What Textract returns instead of a CSV

Textract represents a document as a flat list of typed Block objects joined by parent-to-child relationships. Pages, lines, words, key-value pairs, tables, cells, merged cells, selection elements, queries and layout blocks all live in the same array, and you navigate between them by following ID references. For tables specifically you get:

  • A TABLE block for each detected table, with an EntityTypes value of either STRUCTURED_TABLE or SEMI_STRUCTURED_TABLE. It carries up to four relationship types: CHILD for the cells, MERGED_CELL for the merges, plus TABLE_TITLE and TABLE_FOOTER.
  • A CELL block per cell, with RowIndex, ColumnIndex, RowSpan, ColumnSpan, a Confidence score, geometry, and CHILD relationships pointing at the WORD blocks that make up the text.
  • MERGED_CELL blocks listed separately under the table's MERGED_CELL relationship.
  • TABLE_TITLE and TABLE_FOOTER blocks holding titles and footers detected in or around the table.

A modest table gets large fast. The example in Amazon's own documentation is a small balance sheet, and it produces 65 cells across 13 rows and 5 columns, 9 merged cells, a title block and two footer blocks. Every one of those is a separate entry in the block list with its own UUID.

The merged-cell trap that corrupts most exports

This is the part worth reading twice, because it fails silently and the documentation states it outright:

"The cell block type will always have row span of 1 and column span of 1."

Read that again with a CSV writer in mind. Every CELL block reports RowSpan: 1 and ColumnSpan: 1 regardless of what the document actually shows. A header cell spanning five columns still reports a span of one. The real spans are published somewhere else entirely, on the MERGED_CELL blocks hanging off the table's MERGED_CELL relationship, where you finally see "ColumnSpan": 5.

The obvious way to write the export is to collect the CELL blocks, sort by RowIndex and ColumnIndex, and join with commas. That code compiles, runs, produces a clean file and is wrong. Every merged cell flattens into the single cell at its top-left position, the cells that were covered by the merge come back empty, and depending on how you handle those blanks the columns after the merge can shift by one. Nothing throws. You get a plausible spreadsheet with figures under the wrong headings, which is considerably worse than a crash.

To handle it properly, read the MERGED_CELL blocks first, build a map from each constituent cell ID to its merged parent, and then walk the grid using that map so a merged region is written once and its span is respected.

The header row is not necessarily row one

The second trap is smaller but catches everyone eventually. Textract does tell you which cells are headers, but not by position: it sets EntityTypes to COLUMN_HEADER on those cells. The same field is also used for TABLE_TITLE, TABLE_FOOTER, TABLE_SECTION_TITLE and TABLE_SUMMARY.

That last group matters more than it sounds. When a table has an in-table title, Textract models it as real cells at RowIndex 1, usually merged across every column. Dump the grid without filtering on EntityTypes and your CSV opens with the report's name spread across five columns, the actual column headings on line two, and the footers appended at the bottom as if they were data. Anything downstream that assumes line one is the header now has a title where it expects field names.

How the other major APIs compare

Textract is not unusual here. Of the four major document APIs, exactly one returns a CSV, and it is not the one most people are already using.

ServiceCSV outputHow the table arrives
AWS Textract (AnalyzeDocument, TABLES)NoTABLE and CELL Block objects. Merges published separately.
Amazon Bedrock Data AutomationYesEvery TABLE entity carries representation.csv inline, plus csv_s3_uri on the async API.
Azure AI Document Intelligence (Layout)NoA tables array with rowCount, columnCount and cells carrying rowIndex, columnIndex and columnSpan.
Google Document AI (Form Parser)No from the APIpages[].tables[] split into headerRows and bodyRows. The Document AI Toolbox client library converts a table to a DataFrame on your machine.

The differences are more than cosmetic. Azure puts the span on the cell itself, so a single pass over the cells array is enough and the Textract merge trap does not exist there. Google separates header rows from body rows in the response, so there is no flag to test at all. Bedrock Data Automation goes furthest: a table that continues onto the next page stays a single entity with a page_indices array listing both pages, which is the case every other approach handles worst.

If you are porting an export from one vendor to another, budget for real work. All four services give you the same information and no two of them shape it the same way. The full side-by-side, including how each one marks a header, is on our PDF to CSV reference.

What tables cost, which is the part nobody quotes

Here is the commercial fact that gets missed in project estimates. Textract's cheap headline rate is not the rate you will pay.

DetectDocumentText, the plain OCR operation, is $0.0015 per page in US West (Oregon), which is $1.50 per 1,000 pages. It does not detect tables at all. To get tables you call AnalyzeDocument with FeatureTypes set to TABLES, and that is $0.015 per page for the first million pages, which is $15.00 per 1,000 pages, ten times the text rate. Above a million pages it drops to $0.01. One small consolation: the Layout feature is included free when used with Tables.

The same pattern holds on Azure. Its Read model is $1.50 per 1,000 pages and its documented response contains pages, paragraphs, lines, words and styles, with no tables collection anywhere in it. Tables come from Layout, at $10.00 per 1,000 pages. Google's Enterprise Document OCR is $1.50 per 1,000 and its Form Parser, which does return tables, is $30.00.

So the rows and columns cost between 6.7 and 20 times what the words cost, depending on whose cloud you are on. If somebody sized a table extraction project off the $1.50 figure, the estimate is out by roughly an order of magnitude. We keep every vendor normalized to the same unit on OCR pricing per 1,000 pages, and the page and throughput ceilings you will hit at volume are on AWS Textract limits.

Should you write the CSV writer at all?

Sometimes, yes. If you control the documents, the tables are ruled and consistent, and the output feeds a spreadsheet a person opens, a few hundred lines of code against the Block model is a perfectly good answer and you keep the confidence values.

Reconsider when any of the following is true. If your documents are varied, every new layout becomes a new edge case in your reconstruction logic, and that code never really finishes. If the tables cross page boundaries, as bank statements and long registers do, you are now merging tables across pages by inference. And if the destination is another system rather than a spreadsheet, CSV is probably the wrong target: an invoice has a header and a set of line items, and flattening that to a grid means either repeating the invoice number on every row or splitting it into two files that have to stay in sync.

It is also worth deciding early where the extracted rows are going to live. A CSV on someone's laptop is a staging format, not a destination. Once these tables start loading into a warehouse on a schedule, an upstream change in the extraction quietly changes the numbers in whatever reports sit on top of them, and knowing which dashboards depend on a given table stops being a nice-to-have the first time a figure is questioned in a meeting.

Frequently asked questions

Does AWS Textract output CSV?

No. Textract returns tables as Block objects in JSON, with a TABLE block linked to one CELL block per cell, each carrying RowIndex, ColumnIndex and a confidence value. No Textract operation offers a CSV, XLSX or spreadsheet output. AWS answers the question in its documentation with a code sample called "Exporting Tables into a CSV File", which is to say the conversion is yours to write.

Does AWS Textract output Excel?

No. There is no XLSX output format either. The Textract console lets you download results for a document you uploaded there, but the API returns JSON only. For a programmatic pipeline you build the spreadsheet yourself from the Block objects, or you use Amazon Bedrock Data Automation, which returns a ready-made CSV for every table it detects.

Why are my merged cells wrong in the CSV?

Because you almost certainly iterated CELL blocks. Amazon documents that a CELL block always reports a row span of 1 and a column span of 1, whatever the document shows. The real spans live on separate MERGED_CELL blocks under the table's MERGED_CELL relationship. Read those first and map each constituent cell to its merged parent, or every merge in the document flattens and the columns after it shift.

How much does AWS Textract table extraction cost?

AnalyzeDocument with the TABLES feature is $0.015 per page in US West (Oregon), which is $15.00 per 1,000 pages, falling to $0.01 per page above one million pages. Plain DetectDocumentText is $0.0015 per page, or $1.50 per 1,000, and does not detect tables. Tables therefore cost ten times plain text. Layout is included at no extra charge when used with Tables.

Which AWS service returns CSV directly?

Amazon Bedrock Data Automation. Every TABLE entity in its standard document output carries a representation object holding csv, html, markdown and text versions of the same table, and the asynchronous API additionally writes a csv_s3_uri and one CSV file per table into your output bucket. Note that the synchronous InvokeDataAutomation call does not deliver CSV files to S3, though the inline representation is present in both.

Can Textract extract tables from a scanned PDF?

Yes. That is the point of it. Textract works from the image, so a scan is exactly the case it handles and the case that defeats Camelot, Tabula and pdfplumber, all of which read the embedded text layer or vector ruling lines that a scan simply does not have. Those libraries return zero tables on a scan rather than an error, which is why a pipeline can appear to work in testing and produce nothing in production.

How do I convert Textract output to CSV in Python?

Collect the TABLE blocks, then for each one build a lookup of every block by ID. Read the MERGED_CELL blocks first and record which cell IDs each merge covers along with its real RowSpan and ColumnSpan. Then walk the CHILD cells in RowIndex and ColumnIndex order, skipping any cell already covered by a merge you have written, resolving text through each cell's CHILD WORD blocks, and filtering out cells whose EntityTypes mark them as TABLE_TITLE or TABLE_FOOTER unless you want them in the file.

Is CSV the right output for invoices?

Usually not. An invoice is a record with a header and nested line items, and CSV cannot express that relationship. You either repeat the invoice number on every line or split the document across two files. A typed JSON response keeps the structure and keeps a confidence value on each field, which is what lets you route the uncertain ones to a person instead of discovering the problem downstream.

The short version

Textract does not output CSV and will not start. It returns a rich graph of blocks and expects you to render it. That is a reasonable trade if you want the confidence values and the geometry, and a poor one if all you wanted was a spreadsheet. Before you commit, price the TABLES feature rather than the text rate, and write your reconstruction against a document with a merged header, because that is the case that decides whether your export is correct or merely convincing.

If you would rather not build any of it, DocuOCR returns table rows already reconciled with the confidence values attached, and you can try it on your own PDF before writing anything.

Extract your documents with DocuOCR

DocuOCR's AI OCR software turns any document into clean, structured data in seconds. No template setup required.

Start free

← Back to all articles