Parse, extract, index and answer from one API
Infratex turns PDFs, Word documents and images into clean Markdown, pulls structured fields with evidence, indexes clean context and streams cited answers. Server-side keys, async resources and typed SDKs for Python and Node.
- Base URL
- https://api.infratex.io
- Authentication
- Bearer infratex_sk_...
- SDKs
- pip install infratex
npm install infratex
- ParsePDFs, Word documents or page images into Markdown, with a marker for every page.POST /api/v1/documents
- ExtractStructured fields from a parsed document, with evidence.POST /api/v1/documents/{id}/extractions
- IndexA vector or hybrid retrieval index over the Markdown.POST /api/v1/documents/{id}/indexes
- AnswerStreamed answers that cite the chunks they used.POST /api/v1/responses
Before you start
- Server-side keys
- Direct API calls use
infratex_sk_...bearer tokens. Keep them out of browser code. - Async resources
- Uploads, indexes and extraction runs return
202and expose status endpoints for polling. - Explicit scope
- Search and responses take one scope:
document_ids,collection_idorconversation_id. - Citations by default
- Response streams start with source events before model text, so UIs can render evidence first.
Quickstart
From upload to a cited answer in one file.
Work server-side. Parse a document, index it, then stream a grounded answer from that context. The SDK waits for async steps by default; pass wait=False to manage polling yourself.
Install
Add the SDK to your backend service, or start with curl while wiring env vars.
export INFRATEX_API_KEY=infratex_sk_...Get an API key
Create a key in the dashboard. The full key is shown once, so store it in server secrets.
HTTPAuthorization: Bearer infratex_sk_your_key_hereParse, index and ask
# 1. Parse curl -X POST https://api.infratex.io/api/v1/documents \ -H "Authorization: Bearer $INFRATEX_API_KEY" \ -F "file=@contract.pdf" -F "method=standard" # 2. Index (after status is "parsed") curl -X POST https://api.infratex.io/api/v1/documents/{id}/indexes \ -H "Authorization: Bearer $INFRATEX_API_KEY" \ -H "Content-Type: application/json" -d '{"method":"hybrid"}' # 3. Stream a cited answer curl -N -X POST https://api.infratex.io/api/v1/responses \ -H "Authorization: Bearer $INFRATEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"message":"Summarize termination rights.","method":"hybrid","model":"fast","document_ids":["{id}"],"limit":5}'
SDKs
One resource model, in Python or Node.
Both SDKs expose the same resources (documents, extractions, searches, responses, collections, conversations, account, billing) and the same wait-by-default contract. Python uses snake_case; Node uses camelCase.
from infratex import Infratex
client = Infratex(api_key="infratex_sk_...")
doc = client.documents.upload("board_pack.pdf", method="standard")
client.documents.wait_until_parsed(doc.id)
markdown = client.documents.markdown(doc.id)
print(markdown[:1000])Authentication
One header, scoped to your tenant.
Every /api/v1/* route takes a server-side API key as a bearer token. Create keys in the dashboard; the full infratex_sk_... value is shown once. Keys are stored as SHA-256 hashes and never expire until revoked.
Authorization: Bearer infratex_sk_your_key_hereParsing
PDFs, Word documents and images into clean Markdown.
Upload a PDF, a Word document (.docx) or an ordered image batch. Parsing is asynchronous: upload returns 202 with a pending document, then you poll status or let the SDK wait. The parse method you pick controls the engine, table fidelity and cost.
Methods
| Method | Engine | Tables | Figures | Per page | Best for |
|---|---|---|---|---|---|
standard | Fast multimodal | Markdown pipe tables | Marked with type and caption | 1 credit | Default. Fast, high-quality Markdown for most documents. |
max | Fast multimodal | Markdown pipe tables | Marked, captioned, and described | 3 credits | Documents where charts, diagrams, and photos carry meaning. |
infratex-phi | In-house, self-deployable | OTSL (HTML on request) | Described; chart values as an OTSL table | 3 credits | Dense tables, financial statements, faithfulness-first OCR. |
infratex-gamma | Layout-aware OCR | OTSL with merged cells (HTML on request) | Marked with type; chart values as an OTSL table | 1 credit | Faithful text and tables at volume, with a bounding box for every region. |
standard-ultra-2 | High-fidelity vision | OTSL with merged cells (HTML on request) | Described; chart values as an OTSL table | 3 credits | Complex nested tables and chart-heavy reports. API-only, advanced. |
/api/v1/documents202Upload a PDF or Word document
Multipart file (PDF or .docx) + method + table_format + optional collection_id. Returns a pending document.
/api/v1/documents/images202Upload page images
Repeated files; file order is page order. PNG, JPEG, WebP, up to 50 MB each.
/api/v1/documents/{id}/markdownGet Markdown
Returns extracted Markdown as text/markdown after parsing. table_format=html converts OTSL tables to HTML; figure_descriptions=false drops the model-written figure descriptions.
/api/v1/documents/{id}/layoutGet layout
Bounding boxes per page: label, box and recognised text for every region. infratex-gamma documents only.
/api/v1/documents/{id}/figuresGet figures
Cropped figure images (PNG, 200 dpi) with page, type, detector score and bounding box in PDF points. The matching figure block carries the same src. A page is cropped only when every figure on it is found with confidence and matches its figure blocks one to one; otherwise it gets no crops rather than a wrong one. Download one at /figures/{name}. Any method, PDF input.
/api/v1/documents/{id}Rename or move
Set filename, collection_id, or remove_collection.
/api/v1/parse-enginesList engine versions
Every parse method with the engine versions it accepts, their status and the default.
Upload parametersPOST /api/v1/documents
| Parameter | Type | Default | Description |
|---|---|---|---|
filerequired | file | Single PDF or Word .docx, up to 500 MB. Must be a valid PDF or .docx; see Word documents. | |
method | string | standard | One of standard, max, infratex-phi, infratex-gamma, standard-ultra-2. A method that is temporarily unavailable returns 503 method_unavailable. |
options | json | infratex-gamma only: a JSON object of parse options, listed below. Leave a key out to keep its default. Sent with any other method, it returns 422 invalid_parse_configuration; an unknown key or bad value returns 422 invalid_parse_options naming the field. | |
table_format | enum | otsl | Table syntax the model emits: otsl or html. Honoured by max, standard-ultra-2, infratex-phi and infratex-gamma; standard always returns Markdown pipe tables. Stored with the document and not changeable later: an otsl document can be read back as either syntax, but an html one can only be read back as HTML. |
collection_id | uuid | Assign to an owned collection on upload. | |
engine_version | string | standard-ultra-2 only: the engine version to parse with, 2.0 (stable) or 2.5 (beta). Omitted, the default version is used. The version is pinned on the document; see Engine versions. Sent with another method, it returns 422 engine_version_not_supported; an unknown version returns 422 invalid_engine_version. |
Engine versionsGET /api/v1/parse-engines
standard-ultra-2 lets you pin the engine a document is parsed with, so a run next month uses the same engine as a run today. Send engine_version with the upload (POST /api/v1/documents or /documents/images). The version does not change the price.
| Version | Status |
|---|---|
2.0 | Stable, and the default. |
2.5 | Beta. On dense tables, gives more consistent table structure across runs. |
- Omit
engine_versionand the default version is used:2.0today. - The version a document is parsed with is pinned on it when it is uploaded and shown as
engine_versionon the upload response, onGET /api/v1/documents/{id}and in the list. A parse that is re-queued or resumed keeps it.engine_versionisnullfor other methods and for documents uploaded before engine versions existed. - A pinned version keeps giving the same engine until its deprecation, which we announce in advance. The default only moves to a newer version with advance notice.
2.5is in beta: it may change before it becomes stable.422 engine_version_not_supported: anengine_versionwith a method that has none.422 invalid_engine_version: an unknown version;details.allowedlists the accepted ones. Both are returned before any credits are held.
GET /api/v1/parse-engines lists every method with its versions, their status and which one is the default (an empty versions list for a method without versions).
curl https://api.infratex.io/api/v1/documents \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-F file=@report.pdf -F method=standard-ultra-2 -F engine_version=2.5Word documentsPOST /api/v1/documents
A Word .docx is accepted natively on the same endpoint as a PDF (and in Studio and the MCP create_document tools). It is parsed from the file itself, with no model call, so what Word already knows is kept exactly:
| In the .docx | In the Markdown |
|---|---|
| Headings | Levels from the document's Word styles (outline levels); list and article numbers from its own numbering. |
| Tracked changes | Kept as ~~deleted~~ and <u>inserted</u> text, the way amendment texts print them. Strikethrough and underline in the text are kept too. |
| Tables | Follow table_format (OTSL, or HTML on request), merged cells included. |
| Footnotes | Printed at the bottom of the page that cites them, as <sup>N</sup> lines. |
| Pictures | A <figure type="other"> block captioned with the picture's alt text. |
| Pages | Word's own page breaks (where Word last laid the pages out, plus manual and new-page section breaks) set the <!-- page:N --> markers. Blank pages are dropped and the rest numbered consecutively. A file Word never paginated is one marked page. |
A .docx is billed per page at the standard price, 1 credit a page, whatever method you send; the document reports standard as its method. The billed page_count is the number of marked pages, but at least one page per 6,000 characters of text; a file with a single marked page is billed its saved Word page count, but at least one page per 3,000 characters. So for a .docx, page_count can be higher than the number of <!-- page:N --> markers. A .docx that counts more than 20,000 pages returns 400 invalid_document. Legacy .doc files are not supported yet and return 415 unsupported_media_type (save as .docx or PDF), as do macro-enabled .docm files; a damaged .docx returns 400 invalid_document.
Because nothing is sent to a model, a .docx is the one upload an EU-only account can parse: it is read on our own servers in the EU. The file's content decides, not its name, so a PDF renamed .docx is refused like any PDF. See Data processing location.
Parse optionsinfratex-gamma
Pass these in the options field of an upload as one JSON object, for example -F 'options={"merge_tables": false, "ignore_labels": ["header", "footer"]}'. Studio shows them when Infratex Gamma is selected.
| Parameter | Type | Default | Description |
|---|---|---|---|
detect_orientation | boolean | true | Detect and correct rotated pages. |
unwarp | boolean | false | Flatten curved, folded or photographed pages. |
chart_recognition | boolean | true | Read chart values into a table inside the chart figure. |
image_text | boolean | true | Read the text inside images (logos, screenshots, scanned inserts). |
seal_recognition | boolean | true | Read stamps and seals. |
merge_tables | boolean | true | Join a table that is split across pages. |
relevel_titles | boolean | true | Rebuild heading levels consistently across the document. |
restructure_pages | boolean | true | Rejoin text that runs across pages. |
ignore_labels | string[] | [] | Region kinds to leave out of the Markdown, from the layout labels (for example header, footer, page_number). They stay in the layout. |
layout_detection | boolean | true | Split each page into regions first. Off reads each page as one block of the kind set by task. |
task | enum | ocr | With layout_detection off: ocr, table, formula, chart or seal. |
layout_nms | boolean | true | Drop overlapping duplicate regions. |
layout_shape_mode | enum | auto | Shape of detected regions: auto, rect, quad or poly. |
temperature | number | 0 | 0 to 2. Sampling temperature; 0 is deterministic. |
top_p | number | 1 | Above 0, up to 1. |
repetition_penalty | number | 1 | Above 0, up to 3. |
min_pixels | integer | 147384 | Smallest page-image budget, 3,136 to 16,777,216; not above max_pixels. |
max_pixels | integer | 2822400 | Largest page-image budget, 3,136 to 16,777,216. |
LayoutGET /api/v1/documents/{id}/layout
Documents parsed with infratex-gamma also record where each region sits on the page. Each page gives the width and height of the page image the boxes were measured on, and each region gives a label, a bbox of [x1, y1, x2, y2] in pixels of that image, and the region's recognised content. Divide by width and height to place a box on your own rendering of the page. Labels are title, heading, text, table, chart, figure, formula, caption, footnote, reference, header, footer, page_number and other. Other methods return 404 layout_not_available; a document that is still parsing returns 409.
Markdown parametersGET /api/v1/documents/{id}/markdown
| Parameter | Type | Default | Description |
|---|---|---|---|
table_format | enum | otsl | otsl returns tables exactly as parsed. html converts every OTSL block to a <table> with colspan/rowspan, keeping data-source="chart" on chart data. Markdown pipe tables are returned unchanged. Conversion runs one way only: a document parsed with table_format=html is served as HTML whatever you ask for here, because OTSL cannot be recovered from HTML. |
figure_descriptions | boolean | true | false drops the <description> from every <figure> block. The description is the only text the model writes rather than reads off the page, so with false the Markdown holds only content printed in the document. Each block keeps its tag and attributes (type, src, page-bbox), its <caption> and its chart or pictured-table data. Applied on read, so it works on documents already parsed and combines with table_format. |
Output conventions
Every method returns Markdown with a few machine-readable markers, so you can split by page, find figures, and see what could not be read. Nothing is dropped silently.
| Marker | Meaning |
|---|---|
<!-- page:N --> | Starts page N (1-based, original page numbers). Every page gets one, so you can split the Markdown by page. |
<!-- page:N status=truncated --> | The page's output was cut off. The content that was produced follows the marker. |
<!-- page:N status=failed --> | The page could not be processed after retries. The marker stands alone, and failed pages are not billed. |
<figure type="…">…</figure> | One block per figure, in reading order. type is chart, diagram, photo, table-image, logo, or other. Holds a verbatim <caption> (left out when none is printed), a <description> (max, standard-ultra-2, infratex-phi; the only model-written text, dropped on read with figure_descriptions=false), and for charts with legible values an <otsl data-source="chart"> table (max, standard-ultra-2, infratex-phi, infratex-gamma; a <table data-source="chart"> when parsed with table_format=html). infratex-gamma figures are chart, logo or other and carry no caption or description: a printed caption stays as text next to the block, and text read inside the image is kept inside it. For PDFs, a figure that was cropped also carries src="figures/p<page>-<n>.png" and page-bbox (see GET /api/v1/documents/{id}/figures). |
<otsl>…</otsl> | A table in OTSL (max, standard-ultra-2, infratex-phi, infratex-gamma). Parse with table_format=html to have the model emit <table> markup instead, or request table_format=html on read to convert it. standard emits Markdown pipe tables. |
[table not transcribed] | A visible table whose text is physically illegible (for example a blurred or degraded scan). Tables are otherwise always transcribed in full, however large or dense; this marker is a last resort so a table is never silently left out. |
<!-- toc --> … <!-- /toc --> | Wraps a table of contents or an index. |
$…$ and $$…$$ | Inline math wraps the whole expression in one pair of dollars; display math uses double dollars. |
<mark>…</mark> | Text the page highlights with a coloured band behind the letters, as a highlighter pen marks it (standard-ultra-2). It is not emphasis: highlighted bold text is <mark>**…**</mark>, highlighted regular text is never made bold. Shaded table cells and rows, boxes and banners are not highlights. Kept inside table cells, also with table_format=html. |
<sup>N</sup> | A footnote mark: after the word it annotates (liabilities.<sup>1</sup>) and at the start of the footnote itself (<sup>1</sup> A transfer of …). Every method writes footnote marks this way, however the page raises them. Exponents and units keep their own form (10², m², $x^{2}$), and tables are left as parsed. |
<!-- page:12 -->
## 3.2 Capital ratios
<figure type="chart">
<caption>Figure 3 – CET1 ratio by scenario</caption>
<description>Bar chart of the CET1 ratio (%) under the baseline and adverse scenarios, 2027–2029.</description>
<otsl data-source="chart"><ched>Year<ched>Baseline<ched>Adverse<nl>
<rhed>2027<fcel>15.3<fcel>15.1<nl>
<rhed>2029<fcel>15.6<fcel>11.8<nl></otsl>
</figure>
[table not transcribed]
<!-- page:13 status=failed -->
<!-- page:14 -->
…ProgressGET /api/v1/documents/{id}
While a document is pending or processing, it carries a progress object, on GET /api/v1/documents/{id} and on each document in the list. It is null once the document is parsed or error, and can be null for a moment while processing starts: keep polling.
| Field | Meaning |
|---|---|
stage | queued: waiting for a worker, also after a run stopped and the document went back to the queue. parsing: pages are being read. finishing: every page is through; figures are cropped and the result is stored. |
pages_done | Pages the parser has finished out of pages_total. Pages kept from a run that stopped count as done, and so do pages that failed (they are not billed). |
pages_total | Pages in the document. |
attempt | 0 while the document waits for its first run, then 1. It goes up when a run stopped and the document went back to the queue; a planned restart of our servers does not count. |
max_attempts | Runs allowed. When this many have stopped, the document fails with an error_message saying so. |
updated_at | While queued: when the document entered the queue. While parsing or finishing: when the worker last reported, at least every 30 seconds even when no page finished. More than a couple of minutes old then means the run has stopped (see below). |
{
"id": "2b1f6f0e-3c8e-4d64-9a7c-6d5f0f0d8a11",
"status": "processing",
"page_count": 808,
"error_message": null,
"progress": {
"stage": "parsing",
"pages_done": 412,
"pages_total": 808,
"attempt": 1,
"max_attempts": 3,
"updated_at": "2026-11-10T09:41:07.512Z"
}
}To avoid polling, register a webhook for document.parsed and document.failed.
When processing stalls
If the server running its parse stops (a crash, a restart), the document goes back to the queue on its own: right away for a planned restart, within about six minutes otherwise. The next run picks up the pages already finished, so they are not parsed again (PDFs with standard, max, infratex-phi and standard-ultra-2; infratex-gamma and image batches start again from the first page). attempt shows each such restart, and after max_attempts the document fails with an error_message that says it was interrupted.
Each parse also has a time limit that grows with the page count, up to two hours. When it runs out, the pages already finished are delivered and the rest are marked <!-- page:N status=failed -->; if no page finished, the document fails.
A document is billed once, when it is parsed, for the pages that were parsed. A re-queued or resumed parse is not charged twice, failed pages are not charged, and a document that ends in error is not charged at all: its reserved credits are released and its error_message says what went wrong.
List parametersGET /api/v1/documents
| Parameter | Type | Default | Description |
|---|---|---|---|
status | enum | Filter by pending, processing, parsed, indexed, error. | |
collection_id | uuid | Filter by collection. | |
limit | int | 50 | Page size. |
offset | int | 0 | Pagination offset. |
curl -X POST https://api.infratex.io/api/v1/documents \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-F "file=@report.pdf" \
-F "method=standard" \
-F "collection_id=col_123"
# then, after status is "parsed":
curl https://api.infratex.io/api/v1/documents/{id}/markdown \
-H "Authorization: Bearer $INFRATEX_API_KEY"Extraction
Structured fields, with evidence.
Pull typed fields out of a parsed document. Provide inline inline_fields, or reference a dashboard template_id, exactly one of the two. Set include_evidence to get the source span behind every value.
Field types
| Type | Extra key | Shape |
|---|---|---|
string | { "name": "counterparty", "type": "string", "description": "Legal name" } | |
number | { "name": "fee", "type": "number", "description": "Termination fee" } | |
integer | { "name": "term_months", "type": "integer", "description": "Term length" } | |
boolean | { "name": "auto_renews", "type": "boolean", "description": "Auto-renewal?" } | |
date | { "name": "effective_date", "type": "date", "description": "Effective date" } | |
enum | enum_values | { "type": "enum", "enum_values": ["NY","DE","CA"], ... } |
object | properties | { "type": "object", "properties": [ ...fields ], ... } |
array | items | { "type": "array", "items": { ...field }, ... } |
Every field needs name (1–64, ^[a-zA-Z_][a-zA-Z0-9_]*$) and a description (required). Optional instructions and required refine behaviour. Nest with object.properties and array.items; an array<object> field is what CSV/XLSX export reads. Up to 100 top-level fields per run.
Values come back typed, not as printed: date is normalised to ISO 8601 (YYYY-MM-DD, so 17/04/2025 returns 2025-04-17), and number / integer return JSON numbers (1.354,00 returns 1354). Use string to keep a value exactly as it is printed.
Exhaustive completeness
completeness: "exhaustive" changes how array fields are read; other fields are unaffected.
- A document up to ~30k tokens is read whole, once per array field, so the model sees a summary page and its detail pages together. If that pass fails or the check below finds it skipped many items, the field is read again in slices as for a larger document.
- A larger document is read in ~8k-token slices and every slice is read. An object item that appears in more than one slice (no value disagrees and at least two agree, with numbers compared by value) becomes one item: missing values are filled from the other mention and nested lists are combined. Repeats inside one passage are kept, as are plain-text list items.
- Then one more pass per array field looks for items missing from the list. An item it returns again is merged into the one already found.
Each array field re-reads the document (twice, with the check), so exhaustive runs take longer and bill more input tokens, at the direct rate.
v2 (beta)
Set pipeline: "v2" on a run to use the new extraction engine. The default, legacy, is the current engine; nothing changes unless you ask for v2. The beta is opened per account, on request: ask your Infratex contact to open it for yours. GET /api/v1/extraction-pipelines says whether your account can choose v2 right now (available).
A run is never moved to another engine than the one you asked for. Asking for v2 before the beta is open for your account answers 422 pipeline_not_available; any pipeline other than legacy or v2 answers 422 invalid_pipeline. pipeline goes in the request body (inside extraction when you upload a file): sent as a query parameter, it also answers invalid_pipeline. Nothing is created or billed when a run is refused, and legacy is always accepted.
How v2 reads a document:
- The parsed document is mapped first: every paragraph, table and table cell gets a stable address, such as
p4.t2.r6c3(page 4, table 2, row 6, column 3). - For each top-level
string,number,integerordatefield, the model returns the address it read the value from as well as the value. Rows of anarrayfield carry no address in this beta. - Fixed rules, with no model involved, then check those values against the whole document. A value comes back as
nullwhen it is printed nowhere in the document (allowing for thousands, millions, billions and percentages), when it is a template placeholder, a blank or a form label instead of the value, when it is a zero that is not printed where it was cited, or when it is printed only in a table of contents or in running page headers or footers. Anumberorintegerread from a table also comes back asnullwhen a higher-ranked table prints a different figure for the same line item and period: the financial statements rank above summaries, highlights and key-figures tables, which rank above other tables, and a table whose column headers do not line up with its values (a year printed over the row labels, or a percent-change column that prints amounts) ranks below all of them, its columns not taken to match the field's period. The other table can be anywhere in the document, not only in the part that was read for the field. A table labelled adjusted or pro forma, a table in another currency, or a column for another period (such as a full year for a quarterly field) does not count, and a value whose own row and column match the field can be overruled only by the financial statements. This does not apply when the field asks for such a figure: its name containsadjusted,non_gaap,pro_formaorunderlying(such asadjusted_eps, but notnet_income_non_adjusted), or its description or instructions name a summary as the source after the line item in the field's name or after a verb ("Net income for 2024 from the five-year summary" fornet_income_2024, "Take it from the financial highlights") or ask for the adjusted figure of the field's own line item ("Adjusted EBITDA", "on an adjusted basis"). Text that describes, warns about or excludes summaries or adjusted figures, such as "figures in the summary may be adjusted", "beware of adjusted figures" or "adjusted figures must not be used", is not read as such a request, and neither is a request in text that also excludes them or points to the financial statements: the check then applies. The check matches wording, not meaning: a description that names the field's own line item next to a summary can still be read as a request, so state the source you want plainly. Withinclude_evidence, its evidence carries the reason. - A value that is printed in the document, but not at the address it was cited from, is kept, and its evidence points to where it is printed. Other findings, such as a competing figure in a table of the same or a lower rank, are recorded internally and do not change the value. v2 never replaces a value with another figure.
enum,boolean,objectandarrayfields are not checked by these rules in this beta.- The models, the verification step, the result shape and the pricing are the same as the current engine's.
Because of these rules, a field can come back null where the current engine returned a value. When the same line item is printed with different figures in a multi-year summary and in the income statement, for example, v2 refuses the summary's figure and returns null; it does not switch to the statement's. Known limitations of this beta: v2 does not choose between tables of the same rank that print different figures, such as two financial statements (consolidated and standalone, or a pro forma statement beside the reported one) or two summaries, and keeps the figure it read; a table labelled adjusted or pro forma never overrules another figure, so a figure that only such a table contradicts is kept; and when the parsed document lost the title that marks a table as a financial statement, v2 can still take the figure from the wrong table (a balance sheet is still recognised by its total assets and total liabilities and equity rows). Compare v2 with the current engine on your own documents before you rely on it.
Every run reports the engine that ran in pipeline once it finishes. If v2 cannot run on a document (for example, parsed Markdown above 8 MB), the run uses the current engine, reports pipeline: "legacy" and is billed as a run of the current engine. If the current engine's result for the same document, template and model is already cached, the run reuses it at no charge, as a legacy request would. A later v2 request on a document v2 cannot run on can therefore reuse that result instead of being billed again. In Studio, choose v2 (beta) under Extract → Options; Studio takes PDFs up to 4 MB, so send larger files through the API.
/api/v1/documents/{id}/extractions202Create a run
Inline fields or a template_id. Returns pending.
/api/v1/extractions202Parse and extract in one call
Upload a file and run an extraction on it; wait up to 300 s for the result.
/api/v1/extractions/{run_id}Poll or fetch
Add include_evidence=true for evidence, wait=N (≤300 s) to hold until done.
/api/v1/documents/{id}/extractionsList runs
Prior runs for a document. limit 1–100.
/api/v1/extractions/{run_id}/exportExport tabular
format=xlsx (default) or csv for array<object> fields.
/api/v1/extraction-pipelinesEngines you can choose
The default engine, and whether your account can choose v2 (beta) per run right now. legacy is always available.
Run parametersPOST /api/v1/documents/{id}/extractions
| Parameter | Type | Default | Description |
|---|---|---|---|
inline_fields | FieldDefinition[] | Inline schema. Exactly one of this or template_id. | |
template_id | uuid | Reference a dashboard-managed template. | |
inline_system_prompt | string | Optional guidance (up to 32,000 characters). Only valid with inline_fields. | |
model | string | fast | fast, pro for harder documents, or infratex-phi to run every step on our self-hosted model (see Self-hosted extraction). |
completeness | string | standard | standard or exhaustive. Exhaustive reads array fields from the whole document, merges an item seen in several places into one, then checks for missed items. See Exhaustive completeness. Slower, and bills more tokens. |
include_evidence | bool | false | Return per-field source evidence. |
pipeline | string | legacy | legacy (the current engine) or v2 (beta): top-level string, number, integer and date values cite where they were read and are checked against the document by fixed rules. v2 only once the beta is open for your account (422 pipeline_not_available before that); any other value answers 422 invalid_pipeline. See v2 (beta). |
A run reports status, tier, pipeline (the engine that ran; null until it finishes), input_tokens, output_tokens and credits_charged (null until it finishes; 0 when it failed or was cached), plus the engine_version that computed it. Running the same fields, prompt, model, completeness and pipeline on the same document again returns the earlier result at no charge, with cached: true (except a v2 request on a document v2 could not run on: see v2 (beta)).
Parse and extract in one callPOST /api/v1/extractions
Send the file as multipart/form-data with the run as a JSON string in extraction. The document is parsed and the run starts as soon as the parse ends, so there is one request instead of upload, poll, extract, poll. The parse and the extraction are billed as usual; if the balance can't cover both, the call answers 402 and nothing is created.
| Parameter | Type | Default | Description |
|---|---|---|---|
file | file | A PDF, a .docx, or one PNG, JPEG or WebP image. | |
method | string | standard | Parse method. With model: "infratex-phi" the default is infratex-phi, and only infratex-phi or infratex-gamma are accepted. |
extraction | JSON string | The run body: inline_fields or template_id, inline_system_prompt, model, completeness, include_evidence, pipeline. Checked before anything is uploaded or billed. | |
wait | int | 0 | Seconds (0–300) to hold the request until the run is done or error. Returns 200 with the result if it finished, else 202 with the pending run to poll. |
Self-hosted extraction (infratex-phi)
With model: "infratex-phi", every step of the run (reading the document, verifying each value against its source, the exhaustive passes) runs on Infratex Phi, our own model. No third-party model provider sees the document during extraction. Parse it with infratex-phi to keep the whole pipeline on our own model; POST /api/v1/extractions parses with it by default, and parses tables as HTML, which the model reads directly. These are the engines for on-premises deployments.
- The document is always read directly, never through an index: up to about 36k tokens of Markdown (roughly 50–90 pages) in one pass, larger documents in slices of about 24k tokens, every slice read.
- The fields, their descriptions and the system prompt share the model's window with the document, so together they may take about 12,000 tokens (roughly 80 typical fields). A larger template is refused with 422
instructions_too_long. - Verification reasons before it answers, which adds about 10–20 s to a run.
- If the model is unavailable, runs are refused with 503
model_unavailablebefore anything is reserved.
Pricing
Extraction is billed on the tokens the run reads and writes, across every model call it makes: reading the document, checking each field against its source and, in exhaustive mode, the extra passes. Output includes the model's reasoning on pro. One rate per model, however the document is read: directly in one pass (up to ~30k tokens of Markdown), chunk by chunk with every chunk read (up to ~165k tokens indexed or not, and larger documents with no index), or through a ready vector or hybrid index above ~165k tokens. Credits per 1M tokens (1 credit = €0.005):
| Model | Input | Output |
|---|---|---|
fast | 375 | 2,250 |
pro | 1,250 | 7,500 |
infratex-phi | 375 | 2,250 |
Example: a pro run on a 12-page document that reads 50,000 input tokens and writes 15,000 output tokens costs 50,000 × 1,250 / 1M + 15,000 × 7,500 / 1M = 62.5 + 112.5 = 175 credits (€0.88). The same reading on fast, with 3,000 output tokens, costs 18.75 + 6.75 = 25.5 credits. Every run returns its credits_charged.
infratex-phi always reads directly; its reasoning is included in the output tokens.
curl -X POST https://api.infratex.io/api/v1/documents/{id}/extractions \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "fast",
"include_evidence": true,
"inline_fields": [
{"name": "counterparty", "type": "string", "description": "Legal name of the counterparty"},
{"name": "effective_date", "type": "date", "description": "Contract effective date"}
]
}'
# poll; wait=60 holds the request until the run finishes (up to 60 s)
curl "https://api.infratex.io/api/v1/extractions/{run_id}?include_evidence=true&wait=60" \
-H "Authorization: Bearer $INFRATEX_API_KEY"
# or parse + extract in one call
curl -X POST https://api.infratex.io/api/v1/extractions \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-F file=@contract.pdf \
-F method=standard \
-F wait=180 \
-F 'extraction={"model": "pro", "inline_fields": [{"name": "counterparty", "type": "string", "description": "Legal name of the counterparty"}]}'Indexing
Build retrieval before you search or answer.
Indexing turns parsed Markdown into retrieval artifacts. It's asynchronous: poll GET /api/v1/documents/{id}/indexes/{method} until status is indexed.
/api/v1/documents/{id}/indexes202Create index
Queues vector or hybrid indexing for a parsed document.
/api/v1/documents/{id}/indexes/{method}Get index status
Poll until status is indexed before search or responses.
ParametersPOST /api/v1/documents/{id}/indexes
| Parameter | Type | Default | Description |
|---|---|---|---|
method | string | vector | vector or hybrid. |
curl -X POST https://api.infratex.io/api/v1/documents/{id}/indexes \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Content-Type: application/json" -d '{"method":"hybrid"}'Search
Cited context, no generation.
Search returns ranked chunks for previews, evidence panels and retrieval debugging. Send one scope: document_ids or collection_id, not both.
/api/v1/searchesSearch indexed context
Returns ranked chunks with document, page, score, content, and metadata.
ParametersPOST /api/v1/searches
| Parameter | Type | Default | Description |
|---|---|---|---|
queryrequired | string | 1–4000 characters. | |
method | string | vector | vector or hybrid. |
limit | int | 5 | Results to return. 1–50. |
document_ids | uuid[] | Restrict to these documents. Mutually exclusive with collection_id. | |
collection_id | uuid | Restrict to a collection. Mutually exclusive with document_ids. |
curl -X POST https://api.infratex.io/api/v1/searches \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"Find indemnity carve-outs","method":"hybrid","document_ids":["doc_123"],"limit":5}'Responses
Stream grounded, cited answers.
/api/v1/responsesCreate streaming response
Server-sent events: sources, thinking (when enabled), text deltas, then done.
ParametersPOST /api/v1/responses
| Parameter | Type | Default | Description |
|---|---|---|---|
messagerequired | string | 1–8000 characters. | |
method | string | vector | vector or hybrid. |
model | string | fast | fast or pro. |
reasoning | bool | false | When true, streams thinking events before text. |
limit | int | 5 | Source chunks. 1–20 (tighter than search's 50). |
document_ids | uuid[] | Mutually exclusive with collection_id. | |
collection_id | uuid | Mutually exclusive with document_ids. | |
conversation_id | uuid | Continue a thread. Omit scope selectors: scope comes from the conversation, else 400 conversation_scope_locked. |
Stream events
| Event | Meaning |
|---|---|
sources | Retrieved source chunks, streamed first so UIs can render evidence. |
thinking | Reasoning deltas, only when reasoning=true. |
text | Answer text deltas. |
done | Stream complete. |
error | An error occurred mid-stream. |
curl -N -X POST https://api.infratex.io/api/v1/responses \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message":"What are the top risks?","method":"hybrid","model":"fast","collection_id":"col_123","limit":8,"reasoning":false}'Collections and conversations
Group documents, persist threads.
Collections are a durable scope you can search and answer over together. Conversations persist a multi-turn thread with a fixed scope.
/api/v1/collectionsCreate collection
{ name } (1–255). Group documents for retrieval and responses.
/api/v1/documents/{id}Move document
Set collection_id or remove_collection on a document.
/api/v1/conversationsCreate conversation
{ title?, document_ids? | collection_id? }; the scope fields are mutually exclusive.
/api/v1/conversations/{id}Get conversation
Returns the thread and its messages.
{
"message": "Compare warranty limits across the uploaded agreements.",
"method": "hybrid",
"model": "pro",
"collection_id": "col_123",
"limit": 10,
"reasoning": true
}Webhooks
Know when work finishes, without polling.
Register an HTTPS endpoint and Infratex POSTs a signed JSON event when a parse, an index build or an extraction run finishes or fails. Use it instead of polling GET /api/v1/documents/{id}. Manage endpoints in the dashboard or with your API key.
/api/v1/webhooksRegister an endpoint
{ url, events?, description? }. events empty means every type. The response carries the signing secret, shown only here and on rotation. Up to 10 endpoints per account; the URL must be public HTTPS.
/api/v1/webhooksList endpoints
Your endpoints, without their secrets.
/api/v1/webhooks/{id}Update an endpoint
Any of url, events, description, enabled.
/api/v1/webhooks/{id}Remove an endpoint
Nothing more is sent to it; its delivery log goes too.
/api/v1/webhooks/{id}/rotate-secretRotate the secret
Returns the new secret; deliveries from now on are signed with it.
/api/v1/webhooks/{id}/testSend a test event
Queues a webhook.test event. Up to 60 an hour per account.
/api/v1/webhooks/{id}/deliveriesDelivery log
Newest first, kept 30 days: status, attempts, last HTTP status and error. ?status=pending|delivered|failed, limit up to 100.
/api/v1/webhooks/{id}/deliveries/{delivery_id}/retrySend a delivery again
Same event id and body, a full set of attempts. Failed or delivered ones only (409 while it is still being retried); up to 120 an hour per account.
curl https://api.infratex.io/api/v1/webhooks \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/infratex", "events": ["document.parsed", "document.failed"]}'Events
| Type | Sent when |
|---|---|
document.parsed | A document finished parsing. Fetch its Markdown. |
document.failed | A parse failed. error_message says why; no credits were charged. |
index.completed | An index build finished. The document can be searched. |
index.failed | An index build failed. |
extraction.completed | An extraction run finished. Fetch its result. |
extraction.failed | An extraction run failed. |
webhook.test | Sent when you ask for a test event, whatever types the endpoint takes. |
{
"id": "evt_5f0c3e8a9b7d4c2e8f1a6b3d9e0c7a24",
"type": "document.parsed",
"created_at": "2026-10-22T14:03:11.204Z",
"data": {
"object": "document",
"id": "2b1f6f0e-3c8e-4d64-9a7c-6d5f0f0d8a11",
"status": "parsed",
"filename": "contract.pdf",
"method": "standard",
"page_count": 12,
"collection_id": null,
"error_message": null
}
}data holds the ids and the status; fetch the full document, index or run with its GET. Index events carry document_id, method and chunk_count; extraction events document_id, model and tier.
Verifying signatures
| Header | Value |
|---|---|
Infratex-Signature | t=<unix seconds>,v1=<HMAC-SHA256 hex of "<t>.<raw body>"> with your endpoint's secret. |
Infratex-Event-Id | The event's id, the same on every retry: use it to ignore duplicates. |
Infratex-Event-Type | The event type, also in the body. |
Compute the HMAC over the raw body exactly as received, before any JSON parsing, compare in constant time and reject timestamps more than five minutes old.
import hashlib, hmac, time
def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
fields = dict(part.split("=", 1) for part in header.split(","))
timestamp = int(fields["t"])
if abs(time.time() - timestamp) > tolerance:
return False # too old: a replay
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, fields["v1"])
# Flask: verify(SECRET, request.get_data(), request.headers["Infratex-Signature"])Delivery and retries
Answer with any 2xx within 10 seconds; do the work afterwards. Anything else, a redirect, a timeout or a refused connection is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then marked failed. Delivery is at least once and not ordered: deduplicate on the event id, and read the current state with a GET when order matters.
MCP
Connect Claude and other agents to the same pipeline.
The remote MCP server exposes the full pipeline (parsing, indexing, retrieval and grounded answers) to any MCP client. Claude and ChatGPT connect through OAuth: you sign in to Infratex and approve the connection. Claude Code, Cursor and your own agents can also send an API key. Both reach the same account, with the same credits and billing as the REST API.
https://api.infratex.io/mcpclaude mcp add --transport http infratex https://api.infratex.io/mcp \
--header "Authorization: Bearer infratex_sk_..."
# verify — status should be: ✔ Connected
claude mcp get infratex{
"mcpServers": {
"infratex": {
"type": "http",
"url": "https://api.infratex.io/mcp",
"headers": {
"Authorization": "Bearer infratex_sk_..."
}
}
}
}A typical session: upload a file with create_document_upload, check get_document until the status is parsed, read it with get_document_markdown, or index it with create_index and ask questions with search_documents and answer_documents.
Tools that read
| Tool | What it does |
|---|---|
list_collections | List your collections. |
list_documents | List your documents, filtered by status or collection. |
get_document | A document's status, method and page count. |
get_document_markdown | The parsed Markdown. start_page and end_page return a range of pages; table_format=html converts OTSL tables; figure_descriptions=false drops figure descriptions. |
list_indexes | A document's vector and hybrid indexes. |
get_index | An index's status. |
search_documents | Vector or hybrid retrieval across documents or collections (billed as a search). |
search | Ranked documents for a query, as id, title, text and url (ChatGPT Deep Research format; billed as a search). |
fetch | A document's full text by id (ChatGPT Deep Research format). |
Tools that create
| Tool | What it does |
|---|---|
create_document_upload | A one-time upload URL, so a file is parsed without passing through the conversation. |
create_document | Parse a small PDF or Word .docx sent as base64. |
create_document_images | Parse an ordered batch of images, one page each. |
create_index | Build a vector or hybrid index of a parsed document. |
answer_documents | A cited answer grounded in your indexed documents. |
No tool changes or deletes existing data. Claude asks before it runs a tool that creates something.
Reference
Errors, credits and the endpoint map.
Error codes
Every error has the same body: an error object with a machine-readable code, a message and the request_id to quote when you contact us, plus details when there is more to say (for a validation error, the fields that failed).
{
"error": {
"code": "document_not_ready",
"message": "Document status is 'processing', must be 'parsed' or 'indexed'",
"request_id": "6f1c0e5b2a8d4c51b0e7a9d3c4f2e1a0"
}
}| Status | Meaning |
|---|---|
400 | Bad request, for example conversation_scope_locked, invalid_pdf, encrypted_pdf (a password-protected PDF: remove the password and upload it again), invalid_document (a damaged .docx) or invalid_idempotency_key. |
401 | Missing or invalid API key. |
402 | Insufficient credits. |
403 | Endpoint not available through an API key (dashboard-only). An API key may change data_residency on PATCH /api/v1/account/settings, not the billing settings, and may switch an account to eu_only but not back to default (dashboard_only). |
409 | Not ready: document_not_ready, extraction_not_done, markdown_unavailable (extractions). index_required_for_tier_l only if automatic chunking is disabled on the server. idempotency_request_in_progress: a request with the same Idempotency-Key is still running. |
410 | Gone: output_unavailable. The document's stored output (its markdown, figures and layout) is no longer available, so retrying will not help: upload the document again to parse it. Returned by the markdown, figures, layout and evidence endpoints of the document and by index creation; an extraction run on such a document ends with status error and the same message. |
413 | Payload too large: payload_too_large, context_too_large. |
415 | Unsupported media type, including legacy .doc and macro-enabled .docm files (save as .docx or PDF). |
422 | Validation error: too_many_fields, markdown_unavailable (index creation), instructions_too_long and parse_method_not_supported (infratex-phi extraction). idempotency_key_reused: the Idempotency-Key was used for a different request. engine_version_not_supported: an upload sends engine_version with a method that has no engine versions (only standard-ultra-2 has them). invalid_engine_version: an unknown engine_version; details.allowed lists the accepted versions. invalid_pipeline: an extraction run's pipeline is not legacy or v2, or was sent as a query parameter. pipeline_not_available: the run asks for v2 (beta), which is not open for your account yet. data_policy_violation: the request would be processed where your account's data policy does not allow (an EU-only account parsing anything but a Word .docx with infratex-phi, or asking for an index, a search, an answer or an extraction model that runs outside the EU; with another parse method, an upload that is not a Word .docx returns method_not_allowed). See Data processing location. Returned before any credits are held. |
429 | Rate limited. |
503 | method_unavailable: the parse method is temporarily unavailable; model_unavailable: the infratex-phi extraction model is. Returned before any credits are held. |
Retries and idempotencyIdempotency-Key
A request that times out may still have been accepted. To retry a create safely, send an Idempotency-Key header (any unique string up to 255 characters, a UUID is usual) and reuse it on every retry of that request. Within 24 hours, a retry with the same key and the same request returns the first response, marked Idempotent-Replayed: true, and nothing is created or charged twice. It is accepted on:
POST /api/v1/documentsPOST /api/v1/documents/imagesPOST /api/v1/documents/{id}/indexesPOST /api/v1/documents/{id}/extractionsPOST /api/v1/extractions
Keys belong to your account. While the first request is still running, a retry gets 409 idempotency_request_in_progress with Retry-After. The same key on a different request (another file, other fields) gets 422 idempotency_key_reused. Only successful responses are kept: when a request fails, its key is released, so you can fix the cause and retry with the same key.
curl https://api.infratex.io/api/v1/documents \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Idempotency-Key: 6f1c2a52-7d0e-4c55-9a3e-2f0d1b8c4e77" \
-F file=@contract.pdf -F method=standard| Response | What to do |
|---|---|
Timeout | A network error or no answer: retry. Always safe for GET; for POST, only with the same Idempotency-Key. |
429 | Rate limited: retry after the Retry-After header's seconds. |
409 | idempotency_request_in_progress: the first request is still running, retry after Retry-After. |
5xx | 500, 502, 503 or 504: retry with exponential backoff (1 s, 2 s, 4 s … up to about a minute, with jitter). |
Other 4xx | Do not retry: fix the request. |
Data processing locationPATCH /api/v1/account/settings
Each account chooses where its requests may be processed with data_residency. Read it with GET /api/v1/account/settings and change it with PATCH /api/v1/account/settings, from an API key or from the dashboard (Settings, Data processing location). Any other value than the two below is a 422 validation_error. An API key may change only data_residency; the billing settings stay dashboard-only (403). An API key may switch the account to eu_only (or send eu_only again), but switching an EU-only account back to default is dashboard-only: an API key gets 403 dashboard_only, so a leaked key cannot lift the restriction.
| Value | Meaning |
|---|---|
default | Every parse method and model is available. Some of them process data outside the EU. |
eu_only | A request that would be processed outside the EU is refused with 422 before any credits are held: data_policy_violation, or method_not_allowed for a parse method other than infratex-phi. Word (.docx) uploads are accepted with any method: they are read on our own servers in the EU, with no model call. Every other parse (PDF and image uploads), index, search, answer and extraction model processes data outside the EU today and is refused; a PDF is refused once the file is received, still before any credits are held. Existing documents and results stay readable. Only the dashboard can switch the account back to default. |
curl -X PATCH https://api.infratex.io/api/v1/account/settings \
-H "Authorization: Bearer $INFRATEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data_residency": "eu_only"}'A change applies from the next request. Background work reads the setting when it starts: work already running finishes, and queued work that has not started and needs processing outside the EU fails, with its credits released.
Credits1 credit = €0.005
| Service | Unit | Cost |
|---|---|---|
parse.standard / infratex-gamma | per page | 1 credit a page. |
parse.max / infratex-phi / standard-ultra-2 | per page | 3 credits a page. |
index.vector | per 1M tokens | 20 credits. |
index.hybrid | per 1M tokens | 100 credits. |
search.vector | per search | 2 credits. |
search.hybrid | per search | 10 credits. |
response.fast | per 1M tokens | 1500 in, 9000 out. |
response.pro | per 1M tokens | 5000 in, 30000 out. |
extraction.fast | per 1M tokens | 375 in, 2,250 out, however the document is read. |
extraction.pro | per 1M tokens | 1,250 in, 7,500 out, however the document is read. |
extraction.infratex-phi | per 1M tokens | 375 in, 2,250 out. Always reads directly. |
Endpoint map
Documents
- POST/api/v1/documents
- POST/api/v1/documents/images
- GET/api/v1/documents/{id}
- PATCH/api/v1/documents/{id}
- GET/api/v1/documents/{id}/markdown
- GET/api/v1/documents/{id}/layout
- GET/api/v1/documents/{id}/figures
- DELETE/api/v1/documents/{id}
- GET/api/v1/parse-engines
Retrieval
- POST/api/v1/documents/{id}/indexes
- GET/api/v1/documents/{id}/indexes
- POST/api/v1/searches
- POST/api/v1/responses
Extraction
- POST/api/v1/documents/{id}/extractions
- POST/api/v1/extractions
- GET/api/v1/documents/{id}/extractions
- GET/api/v1/extractions/{run_id}
- GET/api/v1/extractions/{run_id}/export
Webhooks
- POST/api/v1/webhooks
- GET/api/v1/webhooks
- PATCH/api/v1/webhooks/{id}
- DELETE/api/v1/webhooks/{id}
- POST/api/v1/webhooks/{id}/test
- GET/api/v1/webhooks/{id}/deliveries
Account
- GET/api/v1/account
- GET/api/v1/account/settings
- PATCH/api/v1/account/settings
- GET/api/v1/billing
- GET/api/v1/billing/transactions
- GET/api/v1/collections
- POST/api/v1/collections