Invoice Data Extraction Python SDK
Official Python SDK for Invoice Data Extraction. Uploads your files, submits the extraction, waits for it to finish and hands you the rows as data, or a spreadsheet, in a few lines of code.
- Python 3.9 or later
Install
pip install invoicedataextraction-sdk
Quick Start
import json
import os
import sys
from invoicedataextraction import InvoiceDataExtraction
from invoicedataextraction.errors import SdkError, ApiResponseError
try:
client = InvoiceDataExtraction(
api_key=os.environ.get("INVOICE_DATA_EXTRACTION_API_KEY"),
)
result = client.extract(
folder_path="./invoices",
prompt="Extract invoice number, date, vendor name, and total amount",
output_structure="per_invoice",
json_typed_values=True,
console_output=True, # remove to disable console logging
)
if result["status"] == "completed":
for row in client.iterate_results(extraction_id=result["extraction_id"]):
print(row) # one dict per extracted row, keyed by your output columns
except (SdkError, ApiResponseError) as error:
print(json.dumps(error.body, indent=2), file=sys.stderr)
raise SystemExit(1)
extract(...) uploads every supported file in the folder (pass a folder_path or a list of files), submits the extraction, waits until it finishes and returns the result: the final status response from the API, for a completed, failed or cancelled extraction. iterate_results(...) then reads the extracted rows straight from the API, one dict per row, with amounts as numbers and empty cells as None because the extraction was submitted with json_typed_values. Check result["pages"]["failed_count"] to verify that all uploaded pages were processed (when console_output is enabled, failed pages are logged automatically), and result["review_needed"]["count"] for rows that need a human's check before you rely on the data. To get a spreadsheet, add download={"formats": ["xlsx"], "output_path": "./output"} and the file is saved when the extraction completes.
Generate an API key from your dashboard. Every account includes 50 free pages per month. Additional credits can be purchased on a pay-as-you-go basis with no subscription needed.
Constructor
import os
from invoicedataextraction import InvoiceDataExtraction
client = InvoiceDataExtraction(
api_key=os.environ.get("INVOICE_DATA_EXTRACTION_API_KEY"),
)
| Parameter | Required | Description |
|---|---|---|
api_key | Yes | Your API key. |
base_url | No | API base URL. Defaults to https://api.invoicedataextraction.com/v1. Only needed for testing or non-production environments. |
extract(...)
Run a complete extraction in a single call. Pass a folder path or a list of file paths and tell the SDK what to extract; it uploads the files, submits the extraction and waits for it to finish, then returns the result: credits deducted and your remaining balance, successful and failed pages, Review Needed warnings, and prompt notes in ai_uncertainty_notes. Read the extracted rows with get_results(...) or iterate_results(...) afterwards, or pass download to save the output files to disk as Excel, CSV or JSON.
Underlying API workflow: upload session → submit extraction → wait for the result, then download output when download is set. See File limits for size and count constraints.
result = client.extract(
folder_path="./invoices",
prompt="Extract invoice number, date, vendor name, and total amount",
output_structure="per_invoice",
json_typed_values=True,
console_output=True, # remove to disable console logging
)
Parameters
| Parameter | Required | Description |
|---|---|---|
folder_path | One of folder_path or files | Path to a local folder. The SDK uploads every supported file in the folder (.pdf, .jpg, .jpeg, .png). Not recursive. |
files | One of folder_path or files | List of local file paths to upload. Supported types: .pdf, .jpg, .jpeg, .png. |
prompt | Yes | Extraction instructions. String or dict; see Prompt below. |
output_structure | Yes | Controls how the extracted data is structured; see Output structure below. |
task_name | No | Your label for this extraction (3–40 characters). Appears in the web dashboard. If omitted, the SDK generates one as extraction_YYYYMMDD_HHMMSS. |
exclude_columns | No | List of system-generated columns to exclude from output. By default, output files include a "Source File" column indicating which uploaded file/page each row was extracted from, and a "Review Needed" column marking rows that need human verification. If your workflow requires an exact output structure, you can exclude either column. Valid values: "source_file", "review_needed". Excluding "review_needed" removes only the export column; Review Needed warnings can still be generated and returned in the completed response. |
output_language | No | The language of the text the AI writes for you: "automatic" or a language code; see Output options below. Defaults to your account preference. |
review_needed_fill_color | No | Highlight color for Review Needed cells in the XLSX file; see Output options. Defaults to your account preference. |
affected_field_fill_color | No | Highlight color for the XLSX cells a Review Needed warning refers to; see Output options. Defaults to your account preference. |
send_completion_email | No | Boolean. True to be emailed when this extraction finishes; see Output options. Off by default. |
json_typed_values | No | Boolean. True to receive native JSON types (numbers, booleans, None) instead of strings in the JSON output and in the rows get_results(...) returns; recommended for a new integration that reads the rows. See Output options. Off by default. |
ask_questions | No | Boolean. True to let the extraction stop and ask when the documents leave something unsettled; see Questions below. Off by default. |
on_questions | No | Callable called as on_questions(questions, status) when the extraction stops to ask, with the questions and the input_required status they came in; returns the answers and the call carries on. See Questions. |
download | No | Download options; see Download below. If omitted, no files are downloaded. |
polling | No | Polling options; see Polling below. |
console_output | No | Boolean. When True, the SDK logs progress to the console during upload, polling, and download. Off by default. |
on_update | No | Callable for lifecycle updates; see on_update below. |
Output structure
Controls how the extracted data is structured:
| Value | Meaning |
|---|---|
automatic | The AI decides based on your prompt and documents. |
per_invoice | Each invoice becomes a single row (spreadsheet/CSV) or object (JSON). |
per_line_item | Each individual product/service listed within an invoice becomes its own row (spreadsheet/CSV) or object (JSON). |
Prompt
The prompt tells the AI what data to extract. It can be a string or a dict.
String. Describe what you want in natural language (max 2,500 characters):
prompt="Extract invoice number, date, vendor name, and total amount"
With a string, the AI chooses output field names based on your instructions.
Dict. Use a dict when you need exact output field names. Each name is guaranteed to appear exactly as written in the extracted data. You can also add optional per-field and general instructions:
prompt={
"fields": [
{"name": "Invoice Number"},
{"name": "Invoice Date", "prompt": "The date the invoice was issued, NOT the due date"},
{"name": "Vendor Name"},
{"name": "Total Amount", "prompt": "No currency symbol, 2 decimal places"},
],
"general_prompt": "Extract one record per invoice or credit note. Ignore email cover letters. Dates should be in YYYY-MM-DD format.",
}
Each item in fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The name for this data point in the output (2–50 characters). Prefer clear, descriptive names (e.g., "Invoice Number", not "Field A"). |
prompt | string | No | Specific instructions for extracting this data point (3–600 characters). Use this to clarify ambiguities or instruct special handling. |
| Field | Type | Required | Description |
|---|---|---|---|
general_prompt | string | No | Instructions that apply to the full task and across all fields (max 1,500 characters). Use this to provide special handling instructions, specify output formatting, or describe the extraction goal. |
fields must be a non-empty list.
For guidance on writing effective prompts, see the Extraction Guide.
Output options
Five optional keyword arguments shape what the extraction produces. Each applies to this extraction only and leaves your account preferences unchanged; leave one out and the account preference (or the default) applies. All are accepted by extract(...) and submit_extraction(...), and get_extraction(...) returns the values that applied under extraction["options"].
| Parameter | Values | Default | Description |
|---|---|---|---|
output_language | "automatic" or a code below | Account preference (automatic) | The language of the text the AI writes for you: Review Needed messages and prompt notes. With "automatic", the language follows your prompt (your instructions and field names), not the language of your documents, and is English when that is unclear. Extracted values, file names and your field names are never translated. |
review_needed_fill_color | "none", "yellow", "orange", "red" | Account preference (orange) | Highlight color for cells in the Review Needed column that carry a warning. XLSX files only; has no effect when the Review Needed column is excluded. |
affected_field_fill_color | "none", "yellow", "orange", "red" | Account preference (orange) | Highlight color for the extracted data cells a Review Needed warning refers to. XLSX files only; has no effect when the Review Needed column is excluded. |
send_completion_email | True / False | False | Email your account's address when this extraction finishes processing, whether it completes, fails or is cancelled. A submission refused before processing starts (for example for insufficient credits or an encrypted file) sends no email; its failure appears in the returned result. The web app's "Email me when extraction tasks finish" preference does not apply to API submissions. |
json_typed_values | True / False | False | Receive the JSON output with native JSON types instead of strings: amounts, quantities and rates as numbers, yes/no fields as booleans, and a cell with nothing in it as None. Applies to the JSON output file and to the rows get_results(...) returns; XLSX and CSV are unchanged. Fixed at submission. See JSON value types. |
Language codes for output_language:
| Code | Language |
|---|---|
en | English |
ar | Arabic |
zh-Hant | Traditional Chinese |
zh-Hans | Simplified Chinese |
nl | Dutch |
fr | French |
de | German |
el | Greek |
he | Hebrew |
it | Italian |
ja | Japanese |
pl | Polish |
pt | Portuguese |
es | Spanish |
th | Thai |
The SDK checks these values before sending the request; a value outside the sets above is an INVALID_INPUT error whose details["issues"][0]["path"] names the parameter.
Questions
With ask_questions=True, the extraction can stop to ask when the documents leave something unsettled, instead of deciding on its own: which of two names is the supplier, what date format the columns should use. Turn it on when you, or a person watching the dashboard, can answer within a few minutes, and leave it off for a job nobody is watching; the API reference's Input required section says what happens to an unanswered question. Pass on_questions and the SDK calls it with the questions, sends back the answers it returns, and carries on to the result:
def answer(questions, status):
return [
{"question_id": question["question_id"], "accept_recommended": True}
# or {"question_id": ..., "choice_id": "b"}, or {"question_id": ..., "text": "DD/MM/YYYY"}
for question in questions
]
result = client.extract(
folder_path="./invoices",
prompt="Extract invoice number, date, vendor name, and total amount",
output_structure="per_invoice",
json_typed_values=True,
ask_questions=True,
on_questions=answer,
)
Each question is a dict carrying question_id, type (single_choice or free_text), the question in plain words, an example_from_documents when one is visible, a scope (its level is extraction, and applies_to says in words what the answer governs), the choices of a single_choice question (each with a choice_id, a label, cell_would_contain when known, and recommended: True on one of them), and on a free_text question the recommended_approach. An answer names the question_id and gives one of: choice_id (with text beside it to add detail), text alone, or accept_recommended: True. An answer governs the whole extraction, every document in it and not only the example; where the right answer differs by document type, say so in text. The full shape, what happens when nobody answers, and the deadline are in the API reference under Input required.
on_questions is called with (questions, status), the questions and the input_required status they came in, and may be called more than once in one extract(...) call: once for each batch of questions the extraction asks, and again with what still waits if it answered only some of them. If it raises, or if the answers it returned are refused (see answer_questions(...)), extract(...) raises with nothing posted and the extraction keeps waiting: find it in the web dashboard or with list_extractions(status="input_required"), and answer with answer_questions(...). With polling["timeout_ms"] set, the timeout bounds each wait, not the whole call: the time spent in on_questions is not counted, and a new wait starts after the answers are posted.
Without on_questions, extract(...) returns the input_required status as it is, with questions and answer_by; answer with answer_questions(...) and wait again with wait_for_extraction_to_finish(...). The questions also appear in the web dashboard, where a person can answer them; if the questions are not all answered within about four minutes the extraction pauses and the account owner is emailed, and if the questions are not all answered by answer_by the extraction is cancelled with cancellation_reason: "unanswered".
Download
When download is provided, the SDK saves the output files to disk after a successful extraction: the way to get a spreadsheet.
download={
"formats": ["xlsx", "csv", "json"],
"output_path": "./output",
}
| Field | Required | Description |
|---|---|---|
formats | Yes | List of output formats to download. One or more of "xlsx", "csv", "json". |
output_path | Yes | Destination folder for downloaded files. Created automatically if it doesn't exist. |
Downloaded files are named {task_name}_{timestamp}.{format}.
Auto-download is a best-effort convenience. If the extraction completed but a download fails, the SDK surfaces a warning through console_output / on_update and still returns the completed extraction response. You can retry the download later using download_output(...).
Auto-download does not overwrite existing files. If a generated file path already exists, the SDK skips that file and surfaces a warning.
Returns
extract(...) returns the terminal polling response from the API unchanged, for completed, failed, and cancelled extractions. An extraction submitted with ask_questions and no on_questions handler returns the input_required response instead when it stops to ask; see Questions.
Verifying results: When extract(...) returns a completed extraction, check result["pages"]["failed_count"]. If it's 0, every uploaded page was processed successfully and is included in the output. If it's greater than 0, inspect result["pages"]["failed"] and result["pages"]["failure_reasons"] to see which specific files/pages failed and why; those pages are not included in the output. This is the primary check to confirm that everything you submitted was extracted without issue.
Completed:
{
"success": true,
"status": "completed",
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"credits_deducted": 25,
"credits_balance": 125,
"credits_reserved": 0,
"output_structure": "per_invoice",
"output_expires_at": "2026-07-14T10:30:00Z",
"pages": {
"successful_count": 10,
"failed_count": 2,
"successful": [
{ "file_name": "invoice-1.pdf", "page": 1 }
],
"failed": [
{ "file_name": "damaged.pdf", "page": 1 }
],
"failure_reasons": [
{
"code": "PROCESSING_FILE_SIZE_LIMIT_EXCEEDED",
"message": "The upload was accepted, but during processing part of the PDF became too large for our file-processing limit. This can happen when a compressed PDF is processed internally. Split the PDF into smaller page chunks and resubmit.",
"affected_pages": [
{ "file_name": "damaged.pdf", "pages": [1] }
]
}
]
},
"ai_uncertainty_notes": [],
"review_needed": {
"count": 1,
"items": [
{
"message": "Check whether the extracted total should include the handwritten adjustment near the bottom of the document.",
"affected_fields": ["Total Amount"],
"output_row_numbers": [4],
"source_references": ["invoice-1.pdf (Page 2)"]
}
]
},
"output": {
"xlsx_url": "https://...",
"csv_url": "https://...",
"json_url": "https://..."
}
}
| Field | Description |
|---|---|
credits_deducted | Credits charged for this extraction (one credit per successful page). |
credits_balance | Your total credit balance after this extraction was charged (paid plus free credits), the same figure get_credits_balance() returns. Use it to warn before the balance runs out. |
credits_reserved | Credits held by your extractions still being processed. Your usable balance is credits_balance minus credits_reserved. |
output_structure | The output structure used: "per_invoice" or "per_line_item". If you submitted "automatic", this tells you what the AI chose. |
output_expires_at | ISO 8601 timestamp marking when the generated output files will be deleted under the 90-day retention policy. After this time, output.*_url fields are None and download_output(...) raises OUTPUT_EXPIRED. See Output expiry. |
pages.successful_count | Number of pages successfully processed. |
pages.failed_count | Number of pages that failed processing. |
pages.successful | List of successfully processed pages. Each item has file_name (the uploaded file name) and page (the page number within that file). |
pages.failed | List of pages that failed processing. Same shape as successful. |
pages.failure_reasons | Page-failure reason metadata when available. Empty list if none. Each item has code, user-facing message, and affected_pages grouped by uploaded file_name with source-file page numbers. The current public code value is "PROCESSING_FILE_SIZE_LIMIT_EXCEEDED". |
ai_uncertainty_notes | Prompt notes: areas where your prompt left room for interpretation and the AI made an assumption about how to apply it to the documents. Empty list if none. Each note has a topic, a description of what was assumed, and a suggested_prompt_additions list of prompt additions you can use to remove the ambiguity in future extractions. Each suggestion has a purpose (why you'd add it) and instructions (prompt text you can add). |
review_needed | Result-level warnings for records that need human verification before you rely on the output. Always present on completed responses as {"count": ..., "items": [...]}. Check result["review_needed"]["count"]; if greater than 0, route the listed rows for manual verification. Each item has message, affected_fields, output_row_numbers, and source_references. affected_fields is populated only for field-specific concerns. output_row_numbers contains one or more 1-based extracted data row numbers and does not include the Excel/CSV header row. |
output | Presigned download URLs for each format (xlsx_url, csv_url, json_url). None if not available, including when the output has aged past output_expires_at. URLs expire after 5 minutes; use download_output(...) or get_download_url(...) for a fresh URL while output is still retained. |
To work with the extracted rows without downloading a file, call get_results(...) for one page or iterate_results(...) for every row.
File uploads are all-or-nothing: if extract(...) returns without raising, every file was uploaded successfully. The only failures to check for are in pages.failed and pages.failure_reasons, which describe pages that failed during extraction processing. If pages.failed_count is 0, all uploaded files and pages were processed successfully.
We strongly recommend checking result["review_needed"]["count"] before relying on extracted data. If it is greater than 0, route the listed rows for manual verification in your workflow.
Failed:
When the extraction task itself fails, extract(...) returns the failed polling response; it does not raise. The failure details are in the returned response body, not on error.body.
{
"success": false,
"status": "failed",
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits to process this extraction.",
"retryable": false,
"details": { "credits_required": 25, "credits_balance": 15, "credits_reserved": 10 }
}
}
See the API docs for the full list of task failure codes.
Cancelled:
If an extraction is cancelled while queued or processing, from the web app or with cancel_extraction(...), extract(...) returns the cancelled polling response unchanged. No output files are available.
{
"success": true,
"status": "cancelled",
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"credits_deducted": 4,
"cancellation_reason": "user",
"credits_balance": 146,
"credits_reserved": 0
}
credits_deducted covers the work done before the extraction stopped; credits_balance and credits_reserved are as on the completed response. cancellation_reason says why it stopped:
| Value | Meaning |
|---|---|
user | Cancelled from the web app or with cancel_extraction(...). |
unanswered | The extraction asked a question and no answer came by answer_by; see Questions. Submit it again. |
answers_rejected | Three answers to the extraction's questions were refused; the API reference's Answer Questions says what is refused. Submit it again and answer differently. |
When extract(...) raises
extract(...) only raises before a terminal extraction response is available, for example if upload, submission, or polling fails due to invalid input, network errors, or a polling timeout. These are SDK/API errors and are read from error.body as described in Errors.
Staged Workflow
extract(...) runs the full pipeline in one call. If you need control over individual steps, for example uploading files in one part of your system and triggering extraction in another, running multiple extractions against the same uploaded files, or fitting each step into your own error handling and retry logic, use these methods instead:
import json
import os
import sys
from invoicedataextraction import InvoiceDataExtraction
from invoicedataextraction.errors import SdkError, ApiResponseError
try:
client = InvoiceDataExtraction(
api_key=os.environ.get("INVOICE_DATA_EXTRACTION_API_KEY"),
)
upload = client.upload_files(
files=["./invoice1.pdf", "./invoice2.pdf"],
console_output=True,
)
submitted = client.submit_extraction(
upload_session_id=upload["upload_session_id"],
file_ids=upload["file_ids"],
prompt="Extract invoice number and total",
output_structure="per_invoice",
json_typed_values=True,
)
result = client.wait_for_extraction_to_finish(
extraction_id=submitted["extraction_id"],
console_output=True,
)
# Verify all pages were processed
if result["pages"]["failed_count"] > 0:
print("Some pages failed processing:", result["pages"]["failed"])
# Work with the rows directly...
for row in client.iterate_results(extraction_id=submitted["extraction_id"]):
print(row["Invoice Number"], row["Total Amount"])
# ...or download a file
client.download_output(
extraction_id=submitted["extraction_id"],
format="xlsx",
file_path="./output/invoices.xlsx",
)
except (SdkError, ApiResponseError) as error:
print(json.dumps(error.body, indent=2), file=sys.stderr)
raise SystemExit(1)
upload_files(...)
Upload local files without starting an extraction. Use this when you want to upload once and submit extractions separately, for example to run different prompts against the same files, or to upload in one part of your system and extract in another.
Underlying API workflow: create upload session → upload file parts → complete each file. See File limits for size and count constraints.
| Parameter | Required | Description |
|---|---|---|
folder_path | One of folder_path or files | Path to a local folder. The SDK uploads every supported file in the folder (.pdf, .jpg, .jpeg, .png). Not recursive. |
files | One of folder_path or files | List of local file paths to upload. Supported types: .pdf, .jpg, .jpeg, .png. |
upload_session_id | No | Your own session ID. If omitted, the SDK generates one. If an upload fails partway through, that session cannot be resumed; start a new upload with a fresh session ID. |
console_output | No | Boolean. When True, the SDK logs upload progress to the console. |
on_update | No | Callable for upload lifecycle updates; see on_update. |
Returns
{
"upload_session_id": "session_a1b2c3d4-...",
"file_ids": ["file_abc123", "file_def456"]
}
Pass upload_session_id and file_ids to submit_extraction(...) to start an extraction.
File uploads are all-or-nothing. If any file fails to upload, the method raises immediately; there is no partial success state. If upload_files(...) returns without raising, every file was uploaded successfully.
The API checks your credit balance when the upload session is created. If you don't have enough credits, upload_files(...) raises INSUFFICIENT_CREDITS before any files are uploaded.
submit_extraction(...)
Submit an extraction task for files that have already been uploaded. The method returns immediately; it does not wait for the extraction to finish.
Underlying API endpoint: POST /extractions.
| Parameter | Required | Description |
|---|---|---|
upload_session_id | Yes | The upload session ID returned by upload_files(...). |
file_ids | Yes | List of file IDs returned by upload_files(...). |
prompt | Yes | Extraction instructions. String or dict; see Prompt. |
output_structure | Yes | Controls how the extracted data is structured; see Output structure. |
task_name | No | Your label for this extraction (3–40 characters). Appears in the web dashboard. If omitted, the SDK generates one as extraction_YYYYMMDD_HHMMSS. |
exclude_columns | No | List of system-generated columns to exclude from output. By default, output files include a "Source File" column indicating which uploaded file/page each row was extracted from, and a "Review Needed" column marking rows that need human verification. If your workflow requires an exact output structure, you can exclude either column. Valid values: "source_file", "review_needed". Excluding "review_needed" removes only the export column; Review Needed warnings can still be generated and returned in the completed response. |
output_language, review_needed_fill_color, affected_field_fill_color, send_completion_email, json_typed_values | No | The per-extraction output options, exactly as on extract(...); see Output options. |
ask_questions | No | Boolean. True to let the extraction stop and ask; wait_for_extraction_to_finish(...) then returns the input_required response for you to answer with answer_questions(...). See Questions. |
submission_id | No | Your own idempotency ID for this submission. If omitted, the SDK generates one. If a request fails or times out, retry with the same submission_id to safely retrieve the existing task instead of creating a duplicate. |
Returns
{
"success": true,
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"submission_state": "received"
}
The task is now queued for processing. Use extraction_id to poll for results with wait_for_extraction_to_finish(...) or check_extraction(...). Submitted tasks also appear in the web dashboard where you can view progress and results.
wait_for_extraction_to_finish(...)
Wait for an extraction to reach a terminal state (completed, failed, or cancelled), or to stop and ask (input_required, for an extraction submitted with ask_questions). Use this after submit_extraction(...) when you want the SDK to handle the waiting for you. The SDK asks the API to hold each status request until the extraction finishes or 30 seconds pass, so a run that takes three minutes needs about six requests, not a poll every few seconds; see Polling to tune this.
Underlying API endpoint: GET /extractions/{extraction_id}?wait=30 (held by the API, repeated until the extraction finishes).
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID returned by submit_extraction(...). |
polling | No | Polling options; see Polling. |
console_output | No | Boolean. When True, the SDK logs polling progress to the console. |
on_update | No | Callable for waiting lifecycle updates; see on_update. |
Returns
Returns the terminal polling response from the API unchanged, the same shape documented for extract(...) returns, or the input_required response when the extraction stops to ask.
When the extraction completes, you get the full result with credits_deducted, credits_balance, pages, ai_uncertainty_notes, review_needed, and output URLs. Check result["pages"]["failed_count"] to verify all pages were processed, check result["review_needed"]["count"] for result-level warnings before relying on the data, and use ai_uncertainty_notes for prompt assumptions you may want to clarify in future runs. When it fails, you get the failed response with result["error"]["code"] and result["error"]["message"]. If the task is cancelled while the SDK is waiting, from the web app or with cancel_extraction(...), you get the cancelled response with credits_deducted and cancellation_reason. If it stops to ask, you get the input_required response with questions and answer_by: answer with answer_questions(...), then call this method again. In all cases the response is returned, not raised.
If polling.timeout_ms is set and the extraction hasn't finished in time, the method raises SDK_TIMEOUT_ERROR within that time: the hold, the request and the pause between requests are each cut to the time left. The extraction may still be processing; you can check later with check_extraction(...) or from the web dashboard.
get_results(...)
Read one page of the extracted rows as JSON, straight from the API, so you can work with the data without downloading a file. Each row is a dict whose keys are your output columns, exactly as the JSON output file has them, and the Review Needed items for the rows on the page come back alongside. Use iterate_results(...) to walk every row without managing pages yourself.
Underlying API endpoint: GET /extractions/{extraction_id}/results.
page = client.get_results(
extraction_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
limit=100,
)
for row in page["rows"]:
print(row["Invoice Number"], row["Total Amount"])
if page["review_needed"]["count"] > 0:
print("Rows to check:", page["review_needed"]["items"])
Parameters
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction whose rows you want. It must be completed. |
offset | No | Number of rows to skip. Default 0. |
limit | No | Rows per page, from 1 to 1000. Default 100. |
scope | No | "own" or "team". Same semantics as on list_extractions(...). |
Returns
{
"success": true,
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"output_structure": "per_invoice",
"output_expires_at": "2026-07-26T10:30:00Z",
"json_typed_values": true,
"columns": ["Invoice Number", "Invoice Date", "Vendor Name", "Total Amount", "Source File", "Review Needed"],
"rows": [
{
"Invoice Number": "INV-1001",
"Invoice Date": "2026-01-15",
"Vendor Name": "Acme Ltd",
"Total Amount": 1250,
"Source File": "invoice-1.pdf (Page 1)",
"Review Needed": null
}
],
"offset": 0,
"limit": 100,
"row_count": 1,
"total_rows": 1,
"has_more": false,
"next_offset": null,
"review_needed": {
"count": 0,
"items": []
},
"pages": {
"successful_count": 1,
"failed_count": 0,
"successful": [{ "file_name": "invoice-1.pdf", "page": 1 }],
"failed": [],
"failure_reasons": []
}
}
| Field | Description |
|---|---|
json_typed_values | Which mode the rows are in: False when every value is a string, True when the extraction was submitted with json_typed_values and values carry native JSON types; see JSON value types. |
columns | The output column names in order, including Source File and Review Needed unless excluded at submission. |
rows | The rows on this page, one dict per row with a value under every column. Row i of the page (counting from 0) is data row offset + i + 1, the numbering review_needed["items"][...]["output_row_numbers"] uses. |
offset, limit | The page requested. |
row_count | Rows on this page. |
total_rows | Rows in the whole extraction. |
has_more, next_offset | has_more is True when rows remain after this page; next_offset is the offset to request next, None when there are no more. |
review_needed | count is the number of Review Needed items for the whole extraction; items holds only those whose output_row_numbers fall on this page, in the same shape as on extract(...) returns. Check count before relying on the data. |
pages | The page-level results of the extraction, in the same shape as on extract(...) returns. Data from pages["failed"] is missing from the rows. |
output_structure, output_expires_at | As on the completed extraction response. |
The rows of a completed extraction do not change, so pages are stable: request them in order, passing next_offset back as offset until has_more is False. Rows are available for the same 90 days as the output files; output_expires_at says when they stop being.
If the extraction has not completed, the method raises OUTPUT_NOT_AVAILABLE; once it is past its 90-day retention window, OUTPUT_EXPIRED; see Output expiry.
iterate_results(...)
Auto-paginating generator over the rows of get_results(...). Yields one row at a time and fetches the next page when the current one is exhausted. Use this when you want every row without writing the paging loop yourself.
Underlying API endpoint: GET /extractions/{extraction_id}/results (paged).
for row in client.iterate_results(extraction_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890"):
print(row["Invoice Number"], row["Total Amount"])
Parameters
Identical to get_results(...). A caller-provided offset is the row to start from; limit is the page size the iterator fetches with.
Behavior
- Yields individual rows, not pages. Each row is the same dict
get_results(...)returns inrows, in the same order. - Pages are fetched lazily. The iterator does not request the next page until every row of the current page has been yielded. Breaking out of the
forloop early prevents the next page from being fetched. - Mid-stream errors propagate. If a page request fails, the iterator raises on the corresponding
next()call and the consumer'sforloop re-raises. Rows already yielded remain yielded. - Defensive guard on bad pagination state. If the API ever returns
has_more: Truewithout a usablenext_offset, the iterator raisesSDK_HTTP_ERRORrather than risking an infinite loop.
Validation runs eagerly: iterate_results(<invalid kwargs>) raises synchronously at the call site before the generator is returned.
Return type
A generator object (Python iterator). Usable with for ... in ..., list(...), next(...), etc.
download_output(...)
Save one output file of a completed extraction to disk: the way to get a spreadsheet after the staged workflow, or to retry a failed auto-download from extract(...).
Underlying API workflow: request a fresh presigned download URL → download the file → write to disk.
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID whose output you want to download. |
format | Yes | A single output format: "xlsx", "csv", or "json". |
file_path | Yes | Full destination file path on disk. The file extension must match the requested format. The parent directory is created automatically if it doesn't exist. |
download_output(...) does not overwrite existing files. If file_path already exists, the SDK raises SDK_FILESYSTEM_ERROR with guidance to choose a new path or remove the existing file.
The extraction must be completed before downloading. If the output is not available, for example because the extraction hasn't finished or the format was not generated, the method raises OUTPUT_NOT_AVAILABLE. If the output existed but has aged past the 90-day retention window, the method raises OUTPUT_EXPIRED. See Output expiry.
Returns
{
"success": true,
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"format": "xlsx",
"file_path": "./output/invoices.xlsx"
}
check_extraction(...)
Check the current status of a submitted extraction with a single request. Use this when you want to control when each check is made, for example in a job queue where you check on your own schedule rather than having the SDK wait with wait_for_extraction_to_finish(...). Pass wait_seconds and the API holds the request until the extraction finishes or the seconds pass, so one call can replace a loop.
Underlying API endpoint: GET /extractions/{extraction_id}, with ?wait= when wait_seconds is set.
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID to check. |
wait_seconds | No | Whole seconds from 0 to 45, default 0. With a value above 0, the API holds the request until the extraction leaves processing or the time is up, whichever comes first; a response that arrives after the full wait is an ordinary processing response. With 0, the current status is returned immediately. |
Returns
Returns the current polling response from the API unchanged. The response may represent a processing, input_required, completed, cancelled, or failed extraction, the same shapes documented for extract(...) returns and Questions. A processing response includes a progress field (0–100) indicating approximate completion.
check_extraction(...) wraps the polling endpoint and is intended for "is it done yet?" checks. To retrieve the full record (including the original prompt, options, full pages, prompt notes in ai_uncertainty_notes, Review Needed warnings, and the full failure message/details) for any extraction in any state, use get_extraction(...).
get_download_url(...)
Request a fresh presigned download URL for an extraction's output. Use this when you want to handle the download yourself rather than using download_output(...).
Underlying API endpoint: GET /extractions/{extraction_id}/output?format={format}.
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID whose output you want to download. |
format | Yes | A single output format: "xlsx", "csv", or "json". |
Returns
{
"download_url": "https://storage.example.com/...?X-Amz-Signature=...",
"format": "xlsx",
"expires_in_seconds": 300
}
The URL is a temporary, pre-authenticated link. Make a plain GET request to it, with no Authorization header. It expires after 5 minutes.
The extraction must be completed before requesting a download URL. If the output is not available, the method raises OUTPUT_NOT_AVAILABLE. If the output existed but has aged past the 90-day retention window, the method raises OUTPUT_EXPIRED. See Output expiry.
cancel_extraction(...)
Stop an extraction that is still queued or processing. The request is recorded at once and the extraction stops at the next point where it can; wait for it with wait_for_extraction_to_finish(...) or check it with check_extraction(...) until its status is cancelled. credits_deducted on that response covers the work done before it stopped. An extraction that was about to finish may complete instead, in which case it is charged as a completed extraction and its output is available as usual.
Cancelled extractions produce no output files. To remove one and its uploaded files, use delete_extraction(...).
Underlying API endpoint: POST /extractions/{extraction_id}/cancel.
cancel = client.cancel_extraction(extraction_id=extraction_id)
if cancel["status"] == "cancelled":
print("Already stopped:", cancel["cancellation_reason"])
else:
result = client.wait_for_extraction_to_finish(extraction_id=extraction_id)
print(result["status"]) # "cancelled", or "completed" if it finished first
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID to cancel. |
scope | No | "own" or "team". Team admins default to team, so they can cancel any team member's extraction; other callers default to own and may not pass "team". |
Returns
While the extraction is still processing, the response confirms that the request was recorded:
{
"success": true,
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"cancellation_requested": true,
"cancel_requested_at": "2026-09-10T14:03:22.418Z"
}
status stays processing until the extraction has stopped. cancel_requested_at is when the request was first recorded; calling again returns the same result and records nothing new.
If the extraction has already been cancelled, the response is the cancelled polling response documented under extract(...) returns, with cancellation_reason.
An extraction that has already completed or failed cannot be cancelled: the method raises EXTRACTION_NOT_CANCELLABLE, whose details["status"] is "completed" or "failed". Get its result with check_extraction(...) or get_extraction(...).
answer_questions(...)
Answer the questions an extraction stopped to ask; see Questions. Use it when you handle the questions yourself rather than through on_questions: wait_for_extraction_to_finish(...) or check_extraction(...) returned input_required, you answer, then you wait again. Answer every open question in one call, or across several; the extraction continues the moment every open question has an answer.
Underlying API endpoint: POST /extractions/{extraction_id}/answers.
status = client.wait_for_extraction_to_finish(extraction_id=extraction_id)
if status["status"] == "input_required":
answers = [
{"question_id": question["question_id"], "accept_recommended": True}
for question in status["questions"]
]
client.answer_questions(extraction_id=extraction_id, answers=answers)
result = client.wait_for_extraction_to_finish(extraction_id=extraction_id)
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction that is waiting. |
answers | Yes | List with one dict per question you are answering. Each names the question_id and gives one of: choice_id (with text beside it to add detail), text alone (your own words, 1 to 1000 characters, accepted on every question), or accept_recommended: True. |
scope | No | "own" or "team". Team admins default to team, so they can answer for any team member's extraction; other callers default to own and may not pass "team". |
Returns
The extraction's status after the answers, exactly as check_extraction(...) would return it next: processing once every open question has its answer, input_required with the questions still waiting, or whatever the extraction has become. An answer to a question that is no longer waiting changes nothing and returns the current status, so a call can be repeated after a dropped connection. A failed status is returned, not raised, as on check_extraction(...).
A request that could never be right raises INVALID_INPUT, whose details["issues"] name the answer and the field: a question_id the extraction never asked, a choice_id it does not offer, empty or over-long text, accept_recommended sent with choice_id or text, or a question answered twice in one call.
delete_extraction(...)
Permanently delete an extraction, its output files, and its uploaded source files. Use this when you need to remove data immediately rather than waiting for automatic data retention. Extractions that are currently being processed cannot be deleted.
If you created multiple extractions from the same upload session, deleting one will not affect the others: source files are only removed when no other extraction is using them.
Underlying API endpoint: DELETE /extractions/{extraction_id}.
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID to delete. |
Returns
Returns the API response unchanged.
get_credits_balance()
Check your current credit balance and reserved credits.
Underlying API endpoint: GET /credits/balance.
This method takes no arguments.
Returns
{
"success": true,
"credits_balance": 150,
"credits_reserved": 10
}
| Field | Description |
|---|---|
credits_balance | Your total credit balance (paid + free credits). |
credits_reserved | Credits reserved by extractions currently being processed. Your usable balance is credits_balance minus credits_reserved. |
list_extractions(...)
Retrieve a paginated list of your extractions, with optional filters. Items use a slim shape designed for browsing; for the full record (including the original prompt, options, full pages, prompt notes in ai_uncertainty_notes, review_needed, and the full failure message/details) call get_extraction(...) for a specific item.
list_extractions(...) returns a single page. To iterate every matching extraction without writing the cursor loop yourself, use iterate_extractions(...).
Underlying API endpoint: GET /extractions.
page = client.list_extractions(
status="completed",
submission_method="api",
limit=50,
)
for item in page["extractions"]:
print(item["extraction_id"], item["task_name"], item["created_at"])
if page["has_more"]:
next_page = client.list_extractions(
status="completed",
submission_method="api",
limit=50,
cursor=page["next_cursor"],
)
Parameters
All filters are optional. Call list_extractions() with no arguments to list every extraction visible to your API key.
| Parameter | Required | Description |
|---|---|---|
status | No | One of "processing", "completed", "cancelled", or "failed". Filter by current status. cancelled represents tasks cancelled while queued or processing, from the web app or with cancel_extraction(...). |
submission_method | No | "api" or "web_app". Filter by how the extraction was submitted. The web_app value matches the database column verbatim. |
created_after | No | ISO 8601 string or timezone-aware datetime.datetime. Returns extractions created on or after this timestamp. datetime values are serialized via isoformat() before being sent. Naive datetimes are rejected: the API requires an offset. |
created_before | No | ISO 8601 string or timezone-aware datetime.datetime. Returns extractions created on or before this timestamp. |
limit | No | Integer from 1 to 100. The number of items to return per page. |
cursor | No | Opaque pagination token returned as next_cursor from a previous page. Treat it as a string and pass it back unchanged. |
scope | No | "own" or "team". Only relevant for Team accounts. Team admins default to team-visible history; pass "own" to list only your own extractions. Non-admins can omit it. |
For team admins, omitting scope returns team-visible history, equivalent to the dashboard's Team tasks view. Use scope="own" when you want only your own extractions. scope="team" is accepted for explicitness, but only team admins can use it.
Returns
{
"success": true,
"extractions": [
{
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"submission_id": "sub_abc",
"task_name": "March invoices",
"status": "completed",
"created_at": "2026-04-15T10:30:00Z",
"submission_method": "api",
"file_count": 3,
"file_names_preview": ["a.pdf", "b.pdf", "c.pdf"],
"file_names_truncated": false,
"output_structure": "per_invoice",
"credits_deducted": 5,
"available_outputs": ["xlsx", "csv", "json"],
"output_expires_at": "2026-07-14T10:30:00Z"
}
],
"has_more": true,
"next_cursor": "eyJjIjoiMjAyNi0wNC0xNVQxMDozMDowMFoiLCJpIjoxMjM0fQ"
}
When no extractions match, the SDK returns {"success": True, "extractions": [], "has_more": False, "next_cursor": None}.
List item shape
Every list item includes these fields:
| Field | Description |
|---|---|
extraction_id | The extraction's UUID. Use this with get_extraction(...), get_download_url(...), etc. |
submission_id | Your idempotency ID from submit_extraction(...), or None for web-app submissions. |
task_name | The label you gave the extraction at submission, or None. |
status | "processing", "completed", "cancelled", or "failed". cancelled represents a task cancelled while queued or processing, from the web app or with cancel_extraction(...). |
created_at | ISO 8601 timestamp. |
submission_method | "api" or "web_app". |
file_count | Total number of files uploaded for this extraction. |
file_names_preview | The first up to 5 file names, in submission order. Use get_extraction(...) to retrieve the full list. |
file_names_truncated | True when file_count > len(file_names_preview) (i.e., the extraction has more files than fit in the preview). |
output_structure | "per_invoice", "per_line_item", "automatic" (only while an automatic run is still resolving), or None for legacy/unknown rows. |
Status-specific fields:
- Completed items add
credits_deducted(number),available_outputs(a list of"xlsx"/"csv"/"json"indicating which formats can currently be downloaded, empty when the output has aged pastoutput_expires_at), andoutput_expires_at(ISO 8601 string). - Cancelled items represent tasks cancelled while queued or processing, from the web app or with
cancel_extraction(...). They addcredits_deductedandcancellation_reason("user","unanswered"or"answers_rejected"; see the cancelled shape underextract(...)returns); no output files are available. - Processing items add
progress(0-100). - Failed items add
errorwith the slim{"code": ..., "retryable": ...}shape; for the full message and details, callget_extraction(...)on that extraction.
When a team admin lists team-visible history, every item also includes submitted_by with shape {"email": str | None} identifying the team member who created the extraction. The field is absent in own-only listings. The SDK never exposes a user ID.
iterate_extractions(...)
Auto-paginating generator over list_extractions(...). Yields one extraction summary record at a time and transparently fetches the next page when the current one is exhausted. Use this when you want to process every matching extraction without writing the cursor loop yourself.
Underlying API endpoint: GET /extractions (paged).
for extraction in client.iterate_extractions(status="completed"):
print(extraction["extraction_id"], extraction["task_name"])
Parameters
Identical to list_extractions(...), including scope for team-admin listing behavior. A caller-provided cursor is used as the starting point; the iterator manages cursor advancement from that point on.
Behavior
- Yields individual records, not pages. The iterator yields each list item directly, in the same order as
list_extractions(...)would return them. - Pages are fetched lazily. The iterator does not request the next page until every item from the current page has been yielded. Breaking out of the
forloop early (or otherwise terminating the iterator) prevents the next page from being fetched. - Filters are preserved across pages. The original arguments you pass are reused for every page; only
cursoradvances. - Mid-stream errors propagate. If a page request fails, the iterator raises on the corresponding
next()call and the consumer'sforloop re-raises. Items already yielded remain yielded. - Defensive guard on bad pagination state. If the API ever returns
has_more: Truewithout a usablenext_cursor, the iterator raisesSDK_HTTP_ERRORrather than risking an infinite loop.
Validation runs eagerly: iterate_extractions(<invalid kwargs>) raises synchronously at the call site before the generator is returned. You don't need to start iterating to discover bad input.
Return type
A generator object (Python iterator). Usable with for ... in ..., list(...), next(...), etc.
get_extraction(...)
Retrieve a single extraction's full record. The record is the same regardless of state: processing, completed, cancelled, and failed extractions are all returned with success: true and the failure details (when present) on extraction["error"].
Use this when you want the full picture of an extraction: the original prompt and options, the complete file list, page-level results, prompt notes in ai_uncertainty_notes, Review Needed warnings, the full failure error (message and details, not just code and retryable), and available_outputs so you can decide what to download.
get_extraction(...) is record retrieval, distinct from check_extraction(...), which wraps the polling endpoint and is intended for "is it done yet?" checks against in-flight extractions.
Underlying API endpoint: GET /extractions/{extraction_id}/details.
result = client.get_extraction(
extraction_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
)
extraction = result["extraction"]
if extraction["status"] == "failed":
print(extraction["error"]["code"], extraction["error"]["message"])
elif extraction["status"] == "completed":
print("Available formats:", extraction["available_outputs"])
Parameters
| Parameter | Required | Description |
|---|---|---|
extraction_id | Yes | The extraction ID to retrieve. |
Returns
{
"success": true,
"extraction": {
"extraction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"submission_id": "sub_abc",
"task_name": "March invoices",
"status": "completed",
"created_at": "2026-04-15T10:30:00Z",
"submission_method": "api",
"file_count": 3,
"file_names": ["a.pdf", "b.pdf", "c.pdf"],
"output_structure": "per_invoice",
"prompt": "Extract invoice number, date, vendor, total",
"options": {
"exclude_columns": [],
"output_language": "automatic",
"review_needed_fill_color": "orange",
"affected_field_fill_color": "orange",
"send_completion_email": false,
"json_typed_values": false
},
"credits_deducted": 5,
"available_outputs": ["xlsx", "csv", "json"],
"output_expires_at": "2026-07-14T10:30:00Z",
"pages": {
"successful_count": 6,
"failed_count": 0,
"successful": [{ "file_name": "a.pdf", "page": 1 }],
"failed": [],
"failure_reasons": []
},
"ai_uncertainty_notes": [],
"review_needed": {
"count": 0,
"items": []
}
}
}
Record shape
Every record includes these fields:
| Field | Description |
|---|---|
extraction_id, submission_id, task_name, created_at, submission_method, output_structure | Same semantics as the list item shape. |
file_count | Total number of files uploaded. |
file_names | Full list of file names, in submission order (no truncation). |
prompt | The original prompt you submitted: a string, a structured dict ({"fields": [...], "general_prompt": "..."}), an empty string for web-app submissions with no explicit prompt, or None only for legacy/edge rows. |
options | Always present with every field: exclude_columns, output_language, review_needed_fill_color, affected_field_fill_color, send_completion_email and json_typed_values: the values that applied to this extraction, including the account preferences that filled any option left out at submission. See Output options. |
Status-specific fields:
- Completed records add
credits_deducted,available_outputs,output_expires_at, fullpages(withsuccessful,failed, andfailure_reasonslists), prompt notes inai_uncertainty_notes, and result-levelreview_neededwarnings (same shape as onextract(...)returns). - Cancelled records represent tasks cancelled while queued or processing, from the web app or with
cancel_extraction(...). They addcredits_deductedandcancellation_reason; no output files are available. - Processing records add
progress(0-100). - Failed records add
errorwith the full shape{"code": ..., "message": ..., "retryable": ..., "details": ...};get_extraction(...)exposes the full failure information regardless of when the extraction failed.
For team-admin lookups, the record may also include submitted_by with shape {"email": str | None}.
The details endpoint never includes signed download URLs; call download_output(...) or get_download_url(...) to actually download files.
get_extraction(...) does not raise on failed extractions
A failed extraction is a valid record. get_extraction(...) returns it like any other state, with the failure details on extraction["error"]. It only raises for request-level failures: invalid input, authentication errors, EXTRACTION_NOT_FOUND, network failures, etc.
Common workflow: list, then download
Browsing past extractions and downloading their output is a two-step pattern. List/details responses don't include signed download URLs. Instead, use list_extractions(...) or iterate_extractions(...) to find the extraction you want, then call get_download_url(...) (or download_output(...)) for a fresh signed URL when you're ready to download:
for extraction in client.iterate_extractions(
status="completed",
submission_method="api",
):
task_name = extraction.get("task_name") or ""
if task_name.startswith("March invoices"):
if "xlsx" in extraction["available_outputs"]:
client.download_output(
extraction_id=extraction["extraction_id"],
format="xlsx",
file_path=f"./march/{extraction['extraction_id']}.xlsx",
)
else:
# available_outputs is empty when output_expires_at has passed:
# the underlying file has been deleted by the 90-day retention policy.
print(f"Output no longer available for {extraction['extraction_id']}")
break
Working with Output Files
You can control the structure and formatting of all output files in two main ways:
- use
output_structureto choose the top-level record shape, such asper_invoiceorper_line_item - use your prompt to describe the fields, grouping, and overall structure you want, such as "one row per product" or "one row per PO"
You can also use your prompt to:
- specify missing-value placeholders, such as empty string,
N/A, or0 - specify formatting requirements, such as
YYYY-MM-DD, digits only, or no currency symbol - specify the intended output type, such as text, number, date, datetime, boolean, currency, or percentage
Each column is given a type from your prompt and the documents: text, number, currency, percentage, boolean, date or datetime. XLSX uses that type for its cell types; the JSON output uses it as described below; CSV is plain text.
Working with JSON Output
JSON value types
By default, every value in the JSON output file is a string, and so is every value in the rows get_results(...) and iterate_results(...) return, which read the same data. In a column typed as a number, currency or percentage, a value that reads as a number is written as plain digits with a decimal point and no thousands separators or currency symbol ("1234.5"); a value that does not read as a number is written as extracted. Other columns hold the extracted text, and a cell with nothing in it holds the missing-value placeholder from your prompt, or "" when you set none.
Submit with json_typed_values=True (on extract(...) or submit_extraction(...)) and the same file, and the same rows, carry native JSON types instead. Each value is what the spreadsheet cell holds, in JSON's own types (Python's int/float, bool and None once parsed):
| Column type | Value with json_typed_values |
|---|---|
| number, currency | A JSON number (1234.5). A currency column never carries the symbol; ask for a separate currency-code field if you need it. If you ask for the symbol in the value, the column is text and the value stays a string. |
| percentage | A JSON number as the spreadsheet stores it: the fraction when the document shows a percent sign (19% is 0.19), the number as printed when you ask for no sign. |
| boolean | JSON true or false. Ask for true/false values in your prompt. |
| text, date, datetime | A string, in the format you asked for. Ask for YYYY-MM-DD if you want dates you can sort or parse. |
| empty cell | null (None), in every column type. A missing-value placeholder from your prompt is kept as the value: as a number if it reads as one in that column (0 in a currency column is 0), otherwise as the string you gave. |
A value that cannot be read as its column's type stays as the extracted text, so a number column can hold a string. That is deliberate: it lets you see a value that needs a look rather than losing it. Check review_needed as usual; those rows are usually the same ones.
Nested JSON in a field (below) is unaffected: the field is a string containing JSON whose inner values are strings, in either mode.
The option is fixed at submission and applies to this extraction's JSON output only; XLSX and CSV are unchanged. get_results(...) tells you which mode an extraction's rows are in through its json_typed_values field. In string mode, if you plan to parse a value, state the formatting clearly in your prompt: "Do not include currency symbol", "Use digits only", "Return true or false", "Use YYYY-MM-DD format".
Structured JSON fields
You can ask for a field to return structured JSON.
Example prompt:
"prompt": {
"fields": [
{ "name": "Invoice Number" },
{
"name": "Line Items",
"prompt": "Return a JSON array with keys description, quantity, unit_price, and amount. Use digits only for quantity. Use a full stop as the decimal separator. Do not include currency symbols in unit_price or amount. Do not use thousands separators. Use an empty string when a value is missing."
}
]
}
Example JSON output value:
"Line Items": "[{\"description\":\"Widget\",\"quantity\":\"2\",\"unit_price\":\"9.99\",\"amount\":\"19.98\"}]"
In the example above, Line Items is a string whose content is valid JSON.
Use nested line-item JSON like above, mainly for smaller or simpler cases, such as when there are only a few line items and you want a single invoice-level object.
Recommended approach for line items
If you need detailed line item extraction, prefer output_structure: "per_line_item" instead of returning line items inside a nested JSON field.
This is strongly recommended when:
- invoices may contain around 7 or more line items
- line items need detailed per-field instructions
- you want the most reliable line item extraction
In per_line_item, define invoice-level fields and line-item fields as separate top-level fields.
Many workflows can use the per_line_item output directly, with one row/object per line item.
If your workflow needs a nested structure such as { invoice_fields..., line_items: [...] }, include your own stable invoice identifier such as Invoice Number so you can group related line item rows back into invoices in your own system.
Do not rely on Source File alone to group rows into invoices. Source File helps you trace where a row came from, but it is not a stable invoice identifier.
Example prompt for the recommended approach:
{
"prompt": {
"fields": [
{ "name": "Invoice Number" },
{ "name": "Invoice Date", "prompt": "Use YYYY-MM-DD format" },
{ "name": "Vendor Name" },
{ "name": "Line Item Description" },
{ "name": "Line Item Quantity", "prompt": "Use digits only" },
{ "name": "Line Item Unit Price" },
{ "name": "Line Item Amount" }
],
"general_prompt": "For amount fields don't use thousands separators, use full stops as the decimal separator and do not include currency symbols."
},
"output_structure": "per_line_item"
}
Example JSON output rows:
[
{
"Invoice Number": "INV-1001",
"Invoice Date": "2025-01-15",
"Vendor Name": "Acme Ltd",
"Line Item Description": "Widget A",
"Line Item Quantity": "2",
"Line Item Unit Price": "9.99",
"Line Item Amount": "19.98"
},
{
"Invoice Number": "INV-1001",
"Invoice Date": "2025-01-15",
"Vendor Name": "Acme Ltd",
"Line Item Description": "Widget B",
"Line Item Quantity": "1",
"Line Item Unit Price": "5.00",
"Line Item Amount": "5.00"
}
]
Both rows above belong to the same invoice because they share the same Invoice Number. If your workflow needs one record per line item, you can use the rows as-is. If your workflow needs a nested invoice structure, you can group rows that share the same invoice identifier to build your own { invoice_fields..., line_items: [...] } structure.
CSV Output
CSV is a plain-text export. Every value in the CSV file is written as text.
XLSX Output
XLSX uses the most appropriate spreadsheet cell type for each value by default, and follows explicit prompt instructions where provided.
File Limits
| Type | Max size |
|---|---|
| 150 MB | |
| JPG / JPEG / PNG | 5 MB |
| Total batch size | 2 GB |
| Max files per session | 6,000 |
Applies to extract(...) and upload_files(...).
Polling
extract(...) and wait_for_extraction_to_finish(...) accept a polling dict that controls how the SDK waits for an extraction to finish. By default the SDK does not poll in the usual sense: it asks the API to hold each status request until the extraction finishes or 30 seconds pass, and sends the next one as soon as the previous one comes back still processing. A run that takes three minutes therefore costs about six requests instead of eighteen ten-second polls, and you learn of the result within a second of it being ready.
| Field | Default | Description |
|---|---|---|
wait_seconds | 30 | Whole seconds from 0 to 45 for which the API holds each status request. 0 turns holding off and the SDK polls plainly, interval_ms apart. |
interval_ms | 10000 | The minimum spacing between status requests, in milliseconds (minimum 5000). A held request that has already spent it is followed immediately; a request answered sooner is followed after the remainder. |
timeout_ms | None | Maximum time to wait in milliseconds, after which SDK_TIMEOUT_ERROR is raised. None means no timeout. With a timeout set, nothing runs past it: the hold, the request and the pause between requests are each cut to the time left, so the method returns or raises within timeout_ms. |
Each status request has a read timeout of the wait plus 30 seconds (so 60 seconds by default, 30 seconds for a plain poll); a request that outlives it is an SDK_NETWORK_ERROR, which the loop treats like any transient error and retries. A one-off check with the same hold is check_extraction(...) with wait_seconds.
on_update
Optional callable that receives lifecycle updates across all stages. Use this when you want to handle progress reporting yourself, for example to update a UI, feed a progress bar, or route updates to your own logging instead of the built-in console_output.
def on_update(payload):
# payload is a dict with: stage, level, message, progress, extraction_id
print(payload["message"])
| Field | Description |
|---|---|
stage | Current lifecycle stage: "upload", "submission", "waiting", "questions" (the extraction stopped to ask), "download", or "completion". |
level | "info", "warn", or "error". |
message | Human-readable status message. |
progress | Numeric progress when available, otherwise None. |
extraction_id | The extraction ID once available, otherwise None. |
Used by: extract(...), upload_files(...), wait_for_extraction_to_finish(...).
Output expiry
There are two unrelated time limits on output files. Don't confuse them:
| Limit | What expires | Duration | What to do |
|---|---|---|---|
| Signed download URL | The presigned URL itself | 5 minutes | Request a fresh URL via download_output(...) or get_download_url(...) |
| Output file retention | The generated file in storage | 90 days from created_at | Re-run the extraction; the original output is gone |
output_expires_at (on completed responses) tells you when the underlying file will be deleted. After that timestamp:
output["xlsx_url"],output["csv_url"],output["json_url"]on polling/extract responses areNone.available_outputson list/details responses is an empty list.get_download_url(...)anddownload_output(...)raiseOUTPUT_EXPIRED.
OUTPUT_NOT_AVAILABLE is a different error: it means the extraction either hasn't completed, or the requested format was never generated for it. OUTPUT_EXPIRED means the output existed but has aged out of retention.
Conventions
- Method names are snake_case:
extract(...),upload_files(...),submit_extraction(...). - All parameter names are snake_case:
api_key,folder_path,output_structure,task_name,upload_session_id,file_ids,console_output,on_update,file_path. - Response fields are snake_case, matching the API exactly. The SDK returns the same JSON shapes as the raw API: if you have the API docs, those response examples are valid for the SDK too.
- The
filesparameter accepts local file paths as strings only. File objects, byte streams, and in-memory buffers are not supported in v1.
Rate Limits
All API endpoints are rate limited per API key. The SDK automatically retries rate-limited requests, but you should be aware of the limits if you are making many calls. Sustained overuse will result in a RATE_LIMITED error.
| Endpoints | Limit |
|---|---|
| Upload endpoints (create session, get part URLs, complete upload) | 600 requests per minute |
| Submit extraction | 30 requests per minute |
| Poll extraction status | 120 requests per minute |
| Get results | 60 requests per minute |
| List extractions | 60 requests per minute |
| Get extraction details | 60 requests per minute |
| Download output | 30 requests per minute |
| Delete extraction | 30 requests per minute |
| Cancel extraction | 30 requests per minute |
| Check credit balance | 60 requests per minute |
A status request held open with wait_seconds counts as one request, however long it is held.
Errors
SDK methods raise exceptions on failure:
- On failure, a method raises an
SdkError(for SDK-level and validation errors) orApiResponseError(for API response errors). - The structured error body is available on
error.body. error.bodyuses the same JSON error shape as the API.
Note: if you let an exception go uncaught, Python will usually only show the top-level error message in the traceback. To read the full structured SDK/API error payload, catch the exception and inspect error.body.
Error body shape:
{
"success": false,
"error": {
"code": "SOME_ERROR_CODE",
"message": "Human-readable message.",
"retryable": false,
"details": null
}
}
Read the error like this:
from invoicedataextraction import InvoiceDataExtraction
from invoicedataextraction.errors import SdkError, ApiResponseError
try:
client.check_extraction(extraction_id="...")
except (SdkError, ApiResponseError) as error:
print(error.body["error"]["code"])
print(error.body["error"]["message"])
print(error.body["error"]["retryable"])
print(error.body["error"]["details"])
Every error includes a code (machine-readable), message (human-readable), and retryable (whether retrying may succeed). The message is descriptive enough to act on directly in most cases. details provides additional context when available; for example, INVALID_INPUT errors include a details.issues list with the specific validation problems.
INVALID_INPUT can come from either the SDK (caught before the request is sent) or the API. Handle it the same way in both cases.
Authentication errors (UNAUTHENTICATED, API_KEY_EXPIRED, API_KEY_REVOKED) indicate a problem with your API key; generate a new one from your dashboard.
The SDK automatically retries RATE_LIMITED and transient INTERNAL_ERROR responses, and a gateway error between you and the API (a non-JSON 429, 502, 503 or 504 response), but will surface them if retries are exhausted.
Method-specific errors like EXTRACTION_NOT_FOUND, OUTPUT_NOT_AVAILABLE, OUTPUT_EXPIRED, EXTRACTION_IN_PROGRESS, EXTRACTION_NOT_CANCELLABLE, and INSUFFICIENT_CREDITS are documented in the relevant method sections above. For full endpoint-level error details, see the API docs.
Extraction task failure codes
When an extraction task itself fails, the failure code comes from the API-owned extraction failure taxonomy documented in the REST API docs. In the SDK, that code appears on result["error"]["code"] from extract(...), check_extraction(...), and wait_for_extraction_to_finish(...), or on extraction["error"]["code"] from get_extraction(...).
Branch on error["code"], error["retryable"], and any returned error["details"] directly. The SDK does not provide subclasses for individual extraction failure codes.
Task failure vs SDK/API failure:
- After an extraction task has been accepted, the task itself can still finish with
status: "failed". - That is a task outcome, not an SDK error.
check_extraction(...),wait_for_extraction_to_finish(...), andextract(...)return the polling response body for task states such asprocessing,completed,cancelled, andfailed.- When a task ends with
status: "failed", the failure details are in the returned response body, not onerror.body. error.bodyis only used when the SDK method/request itself fails: validation errors, authentication errors, network failures, timeouts, or other operational failures.
SDK-specific error codes:
| Code | When the SDK uses it |
|---|---|
SDK_FILESYSTEM_ERROR | A local filesystem operation failed, such as reading an input file, creating a directory, or writing a downloaded file. |
SDK_NETWORK_ERROR | A network request failed before the SDK received a valid HTTP response, including a status request that outlived its read timeout (see Polling). |
SDK_HTTP_ERROR | The SDK received an unexpected HTTP response shape, such as a non-JSON response or another response that does not match the documented contract. |
SDK_TIMEOUT_ERROR | wait_for_extraction_to_finish(...) timed out before the extraction finished. |
SDK_DOWNLOAD_ERROR | An SDK-managed download step failed. |
SDK_UPLOAD_ERROR | An SDK-managed upload orchestration step failed. |
Method to API Endpoint Mapping
| SDK Method | Underlying API |
|---|---|
extract(...) | upload_files(...) → submit_extraction(...) → wait_for_extraction_to_finish(...), with answer_questions(...) when the extraction asks and on_questions is set, then download_output(...) when download is set |
upload_files(...) | POST /uploads/sessions → POST /uploads/sessions/{id}/parts → POST /uploads/sessions/{id}/complete |
submit_extraction(...) | POST /extractions |
wait_for_extraction_to_finish(...) | GET /extractions/{extraction_id}?wait=30 (held by the API, repeated) |
get_results(...) | GET /extractions/{extraction_id}/results |
iterate_results(...) | GET /extractions/{extraction_id}/results (auto-paginated) |
download_output(...) | GET /extractions/{extraction_id}/output?format={format} → presigned URL download |
check_extraction(...) | GET /extractions/{extraction_id}, with ?wait= when wait_seconds is set |
get_download_url(...) | GET /extractions/{extraction_id}/output?format={format} |
cancel_extraction(...) | POST /extractions/{extraction_id}/cancel |
answer_questions(...) | POST /extractions/{extraction_id}/answers |
delete_extraction(...) | DELETE /extractions/{extraction_id} |
get_credits_balance() | GET /credits/balance |
list_extractions(...) | GET /extractions |
iterate_extractions(...) | GET /extractions (auto-paginated) |
get_extraction(...) | GET /extractions/{extraction_id}/details |