Esc

    Nothing on this page matches “”.

    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
    Code examples in
    1. ParsePDFs, Word documents or page images into Markdown, with a marker for every page.POST /api/v1/documents
    2. ExtractStructured fields from a parsed document, with evidence.POST /api/v1/documents/{id}/extractions
    3. IndexA vector or hybrid retrieval index over the Markdown.POST /api/v1/documents/{id}/indexes
    4. 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 202 and expose status endpoints for polling.
    Explicit scope
    Search and responses take one scope: document_ids, collection_id or conversation_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.

    1. Install

      Add the SDK to your backend service, or start with curl while wiring env vars.

      export INFRATEX_API_KEY=infratex_sk_...
    2. Get an API key

      Create a key in the dashboard. The full key is shown once, so store it in server secrets.

      HTTP
      Authorization: Bearer infratex_sk_your_key_here
    3. Parse, 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.

    Python

    ETL workers, extraction jobs, notebooks and backend APIs. Sync client, waits by default.

    pip install infratex

    Node.js

    Next.js route handlers, Express, queues and streaming backends. Zero dependencies, native fetch.

    npm install infratex

    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.

    HTTP
    Authorization: Bearer infratex_sk_your_key_here

    Do

    Keep keys server-side. Rotate on exposure. Scope work per tenant with separate keys.

    Don't

    Ship keys in browser or mobile bundles. Public, dashboard-only routes (keys, templates, AST) return 403 to API keys.

    Parsing

    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

    MethodEngineTablesFiguresPer pageBest for
    standardFast multimodalMarkdown pipe tablesMarked with type and caption1 creditDefault. Fast, high-quality Markdown for most documents.
    maxFast multimodalMarkdown pipe tablesMarked, captioned, and described3 creditsDocuments where charts, diagrams, and photos carry meaning.
    infratex-phiIn-house, self-deployableOTSL (HTML on request)Described; chart values as an OTSL table3 creditsDense tables, financial statements, faithfulness-first OCR.
    infratex-gammaLayout-aware OCROTSL with merged cells (HTML on request)Marked with type; chart values as an OTSL table1 creditFaithful text and tables at volume, with a bounding box for every region.
    standard-ultra-2High-fidelity visionOTSL with merged cells (HTML on request)Described; chart values as an OTSL table3 creditsComplex nested tables and chart-heavy reports. API-only, advanced.
    POST/api/v1/documents202

    Upload a PDF or Word document

    Multipart file (PDF or .docx) + method + table_format + optional collection_id. Returns a pending document.

    POST/api/v1/documents/images202

    Upload page images

    Repeated files; file order is page order. PNG, JPEG, WebP, up to 50 MB each.

    GET/api/v1/documents/{id}/markdown

    Get 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.

    GET/api/v1/documents/{id}/layout

    Get layout

    Bounding boxes per page: label, box and recognised text for every region. infratex-gamma documents only.

    GET/api/v1/documents/{id}/figures

    Get 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.

    PATCH/api/v1/documents/{id}

    Rename or move

    Set filename, collection_id, or remove_collection.

    GET/api/v1/parse-engines

    List engine versions

    Every parse method with the engine versions it accepts, their status and the default.

    Upload parametersPOST /api/v1/documents

    ParameterTypeDefaultDescription
    filerequiredfileSingle PDF or Word .docx, up to 500 MB. Must be a valid PDF or .docx; see Word documents.
    methodstringstandardOne of standard, max, infratex-phi, infratex-gamma, standard-ultra-2. A method that is temporarily unavailable returns 503 method_unavailable.
    optionsjsoninfratex-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_formatenumotslTable 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_iduuidAssign to an owned collection on upload.
    engine_versionstringstandard-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.

    VersionStatus
    2.0Stable, and the default.
    2.5Beta. On dense tables, gives more consistent table structure across runs.
    • Omit engine_version and the default version is used: 2.0 today.
    • The version a document is parsed with is pinned on it when it is uploaded and shown as engine_version on the upload response, on GET /api/v1/documents/{id} and in the list. A parse that is re-queued or resumed keeps it. engine_version is null for 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.5 is in beta: it may change before it becomes stable.
    • 422 engine_version_not_supported: an engine_version with a method that has none. 422 invalid_engine_version: an unknown version; details.allowed lists 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.5

    Word 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 .docxIn the Markdown
    HeadingsLevels from the document's Word styles (outline levels); list and article numbers from its own numbering.
    Tracked changesKept as ~~deleted~~ and <u>inserted</u> text, the way amendment texts print them. Strikethrough and underline in the text are kept too.
    TablesFollow table_format (OTSL, or HTML on request), merged cells included.
    FootnotesPrinted at the bottom of the page that cites them, as <sup>N</sup> lines.
    PicturesA <figure type="other"> block captioned with the picture's alt text.
    PagesWord'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.

    ParameterTypeDefaultDescription
    detect_orientationbooleantrueDetect and correct rotated pages.
    unwarpbooleanfalseFlatten curved, folded or photographed pages.
    chart_recognitionbooleantrueRead chart values into a table inside the chart figure.
    image_textbooleantrueRead the text inside images (logos, screenshots, scanned inserts).
    seal_recognitionbooleantrueRead stamps and seals.
    merge_tablesbooleantrueJoin a table that is split across pages.
    relevel_titlesbooleantrueRebuild heading levels consistently across the document.
    restructure_pagesbooleantrueRejoin text that runs across pages.
    ignore_labelsstring[][]Region kinds to leave out of the Markdown, from the layout labels (for example header, footer, page_number). They stay in the layout.
    layout_detectionbooleantrueSplit each page into regions first. Off reads each page as one block of the kind set by task.
    taskenumocrWith layout_detection off: ocr, table, formula, chart or seal.
    layout_nmsbooleantrueDrop overlapping duplicate regions.
    layout_shape_modeenumautoShape of detected regions: auto, rect, quad or poly.
    temperaturenumber00 to 2. Sampling temperature; 0 is deterministic.
    top_pnumber1Above 0, up to 1.
    repetition_penaltynumber1Above 0, up to 3.
    min_pixelsinteger147384Smallest page-image budget, 3,136 to 16,777,216; not above max_pixels.
    max_pixelsinteger2822400Largest 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

    ParameterTypeDefaultDescription
    table_formatenumotslotsl 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_descriptionsbooleantruefalse 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.

    MarkerMeaning
    <!-- 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.
    Markdown
    <!-- 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.

    FieldMeaning
    stagequeued: 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_donePages 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_totalPages in the document.
    attempt0 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_attemptsRuns allowed. When this many have stopped, the document fails with an error_message saying so.
    updated_atWhile 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).
    JSON
    {
      "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

    ParameterTypeDefaultDescription
    statusenumFilter by pending, processing, parsed, indexed, error.
    collection_iduuidFilter by collection.
    limitint50Page size.
    offsetint0Pagination 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

    TypeExtra keyShape
    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" }
    enumenum_values{ "type": "enum", "enum_values": ["NY","DE","CA"], ... }
    objectproperties{ "type": "object", "properties": [ ...fields ], ... }
    arrayitems{ "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, integer or date field, the model returns the address it read the value from as well as the value. Rows of an array field 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 null when 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. A number or integer read from a table also comes back as null when 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 contains adjusted, non_gaap, pro_forma or underlying (such as adjusted_eps, but not net_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" for net_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. With include_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, object and array fields 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.

    POST/api/v1/documents/{id}/extractions202

    Create a run

    Inline fields or a template_id. Returns pending.

    POST/api/v1/extractions202

    Parse and extract in one call

    Upload a file and run an extraction on it; wait up to 300 s for the result.

    GET/api/v1/extractions/{run_id}

    Poll or fetch

    Add include_evidence=true for evidence, wait=N (≤300 s) to hold until done.

    GET/api/v1/documents/{id}/extractions

    List runs

    Prior runs for a document. limit 1–100.

    GET/api/v1/extractions/{run_id}/export

    Export tabular

    format=xlsx (default) or csv for array<object> fields.

    GET/api/v1/extraction-pipelines

    Engines 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

    ParameterTypeDefaultDescription
    inline_fieldsFieldDefinition[]Inline schema. Exactly one of this or template_id.
    template_iduuidReference a dashboard-managed template.
    inline_system_promptstringOptional guidance (up to 32,000 characters). Only valid with inline_fields.
    modelstringfastfast, pro for harder documents, or infratex-phi to run every step on our self-hosted model (see Self-hosted extraction).
    completenessstringstandardstandard 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_evidenceboolfalseReturn per-field source evidence.
    pipelinestringlegacylegacy (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.

    ParameterTypeDefaultDescription
    filefileA PDF, a .docx, or one PNG, JPEG or WebP image.
    methodstringstandardParse method. With model: "infratex-phi" the default is infratex-phi, and only infratex-phi or infratex-gamma are accepted.
    extractionJSON stringThe run body: inline_fields or template_id, inline_system_prompt, model, completeness, include_evidence, pipeline. Checked before anything is uploaded or billed.
    waitint0Seconds (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):

    ModelInputOutput
    fast3752,250
    pro1,2507,500
    infratex-phi3752,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.

    vector

    Vector embeddings over chunks. Good default for natural-language questions. 20 credits / 1M tokens.

    hybrid

    Vector + keyword + document-structure (AST) retrieval. Recommended for contracts, filings, and tables. 100 credits / 1M tokens.

    POST/api/v1/documents/{id}/indexes202

    Create index

    Queues vector or hybrid indexing for a parsed document.

    GET/api/v1/documents/{id}/indexes/{method}

    Get index status

    Poll until status is indexed before search or responses.

    ParametersPOST /api/v1/documents/{id}/indexes

    ParameterTypeDefaultDescription
    methodstringvectorvector 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"}'

    Responses

    Stream grounded, cited answers.

    fast

    Lower-latency model for product surfaces, summaries, and routine Q&A.

    pro

    Higher-capability model for complex synthesis, legal analysis, and cross-document questions.

    POST/api/v1/responses

    Create streaming response

    Server-sent events: sources, thinking (when enabled), text deltas, then done.

    ParametersPOST /api/v1/responses

    ParameterTypeDefaultDescription
    messagerequiredstring1–8000 characters.
    methodstringvectorvector or hybrid.
    modelstringfastfast or pro.
    reasoningboolfalseWhen true, streams thinking events before text.
    limitint5Source chunks. 1–20 (tighter than search's 50).
    document_idsuuid[]Mutually exclusive with collection_id.
    collection_iduuidMutually exclusive with document_ids.
    conversation_iduuidContinue a thread. Omit scope selectors: scope comes from the conversation, else 400 conversation_scope_locked.

    Stream events

    EventMeaning
    sourcesRetrieved source chunks, streamed first so UIs can render evidence.
    thinkingReasoning deltas, only when reasoning=true.
    textAnswer text deltas.
    doneStream complete.
    errorAn 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.

    POST/api/v1/collections

    Create collection

    { name } (1–255). Group documents for retrieval and responses.

    PATCH/api/v1/documents/{id}

    Move document

    Set collection_id or remove_collection on a document.

    POST/api/v1/conversations

    Create conversation

    { title?, document_ids? | collection_id? }; the scope fields are mutually exclusive.

    GET/api/v1/conversations/{id}

    Get conversation

    Returns the thread and its messages.

    JSON
    {
      "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.

    POST/api/v1/webhooks

    Register 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.

    GET/api/v1/webhooks

    List endpoints

    Your endpoints, without their secrets.

    PATCH/api/v1/webhooks/{id}

    Update an endpoint

    Any of url, events, description, enabled.

    DELETE/api/v1/webhooks/{id}

    Remove an endpoint

    Nothing more is sent to it; its delivery log goes too.

    POST/api/v1/webhooks/{id}/rotate-secret

    Rotate the secret

    Returns the new secret; deliveries from now on are signed with it.

    POST/api/v1/webhooks/{id}/test

    Send a test event

    Queues a webhook.test event. Up to 60 an hour per account.

    GET/api/v1/webhooks/{id}/deliveries

    Delivery log

    Newest first, kept 30 days: status, attempts, last HTTP status and error. ?status=pending|delivered|failed, limit up to 100.

    POST/api/v1/webhooks/{id}/deliveries/{delivery_id}/retry

    Send 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.

    bash
    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

    TypeSent when
    document.parsedA document finished parsing. Fetch its Markdown.
    document.failedA parse failed. error_message says why; no credits were charged.
    index.completedAn index build finished. The document can be searched.
    index.failedAn index build failed.
    extraction.completedAn extraction run finished. Fetch its result.
    extraction.failedAn extraction run failed.
    webhook.testSent when you ask for a test event, whatever types the endpoint takes.
    JSON
    {
      "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

    HeaderValue
    Infratex-Signaturet=<unix seconds>,v1=<HMAC-SHA256 hex of "<t>.<raw body>"> with your endpoint's secret.
    Infratex-Event-IdThe event's id, the same on every retry: use it to ignore duplicates.
    Infratex-Event-TypeThe 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.

    Endpoint
    https://api.infratex.io/mcp

    Claude (web, Desktop, mobile)

    Customize → Connectors → Add custom connector. Paste the endpoint, select Connect, sign in to Infratex and approve.

    ChatGPT

    Add a custom connector with the endpoint and OAuth authentication, then sign in and approve.

    Claude Code, Cursor, agents

    Send Authorization: Bearer infratex_sk_... with a key from Dashboard → API Keys.

    Claude Code
    claude mcp add --transport http infratex https://api.infratex.io/mcp \
      --header "Authorization: Bearer infratex_sk_..."
    
    # verify — status should be: ✔ Connected
    claude mcp get infratex
    Cursor and other clients
    {
      "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

    ToolWhat it does
    list_collectionsList your collections.
    list_documentsList your documents, filtered by status or collection.
    get_documentA document's status, method and page count.
    get_document_markdownThe 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_indexesA document's vector and hybrid indexes.
    get_indexAn index's status.
    search_documentsVector or hybrid retrieval across documents or collections (billed as a search).
    searchRanked documents for a query, as id, title, text and url (ChatGPT Deep Research format; billed as a search).
    fetchA document's full text by id (ChatGPT Deep Research format).

    Tools that create

    ToolWhat it does
    create_document_uploadA one-time upload URL, so a file is parsed without passing through the conversation.
    create_documentParse a small PDF or Word .docx sent as base64.
    create_document_imagesParse an ordered batch of images, one page each.
    create_indexBuild a vector or hybrid index of a parsed document.
    answer_documentsA 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"
      }
    }
    StatusMeaning
    400Bad 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.
    401Missing or invalid API key.
    402Insufficient credits.
    403Endpoint 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).
    409Not 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.
    410Gone: 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.
    413Payload too large: payload_too_large, context_too_large.
    415Unsupported media type, including legacy .doc and macro-enabled .docm files (save as .docx or PDF).
    422Validation 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.
    429Rate limited.
    503method_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/documents
    • POST /api/v1/documents/images
    • POST /api/v1/documents/{id}/indexes
    • POST /api/v1/documents/{id}/extractions
    • POST /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
    ResponseWhat to do
    TimeoutA network error or no answer: retry. Always safe for GET; for POST, only with the same Idempotency-Key.
    429Rate limited: retry after the Retry-After header's seconds.
    409idempotency_request_in_progress: the first request is still running, retry after Retry-After.
    5xx500, 502, 503 or 504: retry with exponential backoff (1 s, 2 s, 4 s … up to about a minute, with jitter).
    Other 4xxDo 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.

    ValueMeaning
    defaultEvery parse method and model is available. Some of them process data outside the EU.
    eu_onlyA 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

    ServiceUnitCost
    parse.standard / infratex-gammaper page1 credit a page.
    parse.max / infratex-phi / standard-ultra-2per page3 credits a page.
    index.vectorper 1M tokens20 credits.
    index.hybridper 1M tokens100 credits.
    search.vectorper search2 credits.
    search.hybridper search10 credits.
    response.fastper 1M tokens1500 in, 9000 out.
    response.proper 1M tokens5000 in, 30000 out.
    extraction.fastper 1M tokens375 in, 2,250 out, however the document is read.
    extraction.proper 1M tokens1,250 in, 7,500 out, however the document is read.
    extraction.infratex-phiper 1M tokens375 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