Docsfra
API Documentation
v1

Extraction & Artifacts

Where parsing runs: document parsing (page reading + layout) runs on Docsfra's own GPU infrastructure — page images are never sent to third-party AI providers. Only the extracted text reaches the configured model lanes (or your own BYOK endpoints).

When a document finishes processing, Docsfra doesn't just give you raw text back — it builds an AI-native representation (AIDR) of the document: a structured "identity card" (memory), the tables and figures found on its pages (structured objects), a graph of the entities and relations mentioned in it (entity graph), the logical dependencies stated within it (reasoning graph), and an index of everything available for that document (artifacts). This section covers the endpoints that expose that representation.

Representation → artifact

Internally, a document's representation is a single JSON object with independent layers (memory, structured_objects, entity_graph, ...) that are filled in as each extraction step completes. Each layer is best-effort: if a layer failed to generate (LLM error, or the document predates the representation pipeline), it's simply absent — this never blocks delivery of the base document (markdown/JSON export). What you read through the endpoints below is a read view onto that representation, not a separate artifact store.

EndpointLayerWhat it returns
GET /v1/documents/{id}/memorymemoryThe document's identity card: title, type, purpose, summary, topics, section tree
GET /v1/documents/{id}/objectsstructured_objectsTables (row/cell JSON) and figures found in the document
GET /v1/documents/{id}/line-itemsstructured_objects (derived)Typed, deduplicated invoice/customs line items derived from structured_objects tables, plus reconciliation
GET /v1/documents/{id}/entitiesentity_graphTyped entity nodes and the relations (edges) between them
GET /v1/documents/{id}/reasoningreasoning_graphLogical dependency nodes (obligations, conditions, penalties, ...) and the relations (edges) between them
GET /v1/documents/{id}/extractmemory + objects + entities + reasoningAll four layers combined in a single response
GET /v1/documents/{id}/artifactsA catalog of every AI artifact for the document (including the five above), with availability flags

Output language

Among the layers produced at upload time, the fields that contain narrative text — the memory layer's summary/purpose fields (summary, purpose) and the reasoning_graph's claim text (statement) — are written in the tenant's output language, as selected in the Console under Models at the time of processing. entity_graph values (canonical_name, attributes) are unaffected by this setting — they are values extracted verbatim from the document and stay in the source document's language. Unlike /v1/ask and /v1/cross-check (see Ask (RAG) → Output language), the language of these layers cannot be overridden per request, since generation happens at upload time, before any query is made. Important: changing the output language setting only affects new uploads going forward — previously processed documents are not automatically reprocessed; their existing memory/reasoning layers stay in whatever language they were generated in.

Auth and entitlement

All endpoints in this section require Authorization: Bearer <api_key> and are gated by the extract module entitlement — a key not authorized for extract gets 403 service_not_enabled (see Getting Started → Entitlement). Unlike search and ask, none of these endpoints are metered — reading extraction results doesn't deduct credits, no matter how many times you call them.

Pricing

Reading is free (see above) — what's billed is the one-time production of an artifact, the first time Docsfra successfully generates it for a document:

ModuleArtifactUnitDefault rate
memoryDocument Memory (identity card)document0.10
entitiesEntity Graphdocument0.15
reasoningReasoning Graphdocument0.15
layoutLayout Model (bounding boxes)page0.05

Each module is charged once per document (or once per page, for layout) only when that artifact is actually produced — a failed production attempt (LLM error, or the module not enabled for the tenant's plan) is never charged; the layer is simply absent from the representation, same as described above. structured_objects (tables/figures) has no separate production charge: table/figure extraction is deterministic and included in the base pipeline. These rates can be overridden per tenant, same as the module rates in Getting Started → Metering and credits — check the dashboard's usage/pricing screen for your tenant's current rates. Reiterating: the read endpoints on this page are unmetered regardless of these production rates — you can call /memory, /entities, /reasoning, /layout, /extract, and /artifacts as many times as you like at no cost once an artifact exists.

Memory (identity card)

GET /v1/documents/{document_id}/memory
curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/memory \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "memory": {
    "title": "Bill of Lading — INV-001",
    "doc_type": "bill_of_lading",
    "purpose": "Proof of shipment and title transfer for a container of goods",
    "summary": {
      "one_line": "Ocean bill of lading for a 40ft container shipped from Izmir to Rotterdam.",
      "paragraph": "This document is the bill of lading covering shipment INV-001, ..."
    },
    "language": "en",
    "entities": [
      { "name": "Acme Logistics A.S.", "type": "company" },
      { "name": "Rotterdam Port Authority", "type": "organization" }
    ],
    "topics": ["shipping", "logistics", "container freight"],
    "timeline": [
      { "date": "2026-03-14", "event": "Goods loaded on board" }
    ],
    "definitions": [],
    "references": [
      { "kind": "purchase_order", "target": "PO-88231" }
    ],
    "section_tree": [
      { "path": "1", "title": "Shipper / Consignee" },
      { "path": "2", "title": "Cargo Description" }
    ],
    "segments": [
      {
        "doc_type": "bill_of_lading",
        "title": "Bill of Lading — INV-001",
        "page_start": 1,
        "page_end": 4,
        "one_line": "Ocean bill of lading for a 40ft container shipped from Izmir to Rotterdam."
      }
    ]
  },
  "generated_at": "2026-03-14T10:22:03Z",
  "prompt_version": "mem-v1"
}

memory.entities here is a lightweight name/type list meant for a quick read — it is not the full entity graph (see Entities below). section_tree is built deterministically from the document's section headings (not LLM-generated), so it's always present when the document was chunked with sections.

Multi-document / merged files

A single upload can be a scan that merges several logically distinct documents (e.g. a bill of lading followed by a packing list and an invoice, scanned back-to-back into one file). memory.segments is how that gets surfaced: each entry is one logical document, identified by its own doc_type, title, one_line summary, and the page range (page_startpage_end, 1-indexed and inclusive) it occupies within the merged file. For a single-document upload, segments has exactly one element covering the whole page range.

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404memory_not_availableDocument exists but memory hasn't been generated yet (no representation, or the memory layer is empty)

Objects (tables and figures)

GET /v1/documents/{document_id}/objects
curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/objects \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "objects": {
    "tables_count": 1,
    "figures_count": 0,
    "truncated": false,
    "tables": [
      {
        "object_id": "tbl-3-0",
        "page": 3,
        "caption": "Cargo Description",
        "columns": ["Item", "Qty", "Weight (kg)"],
        "rows": [
          ["Machine parts", "12", "480"],
          ["Spare parts kit", "3", "60"]
        ],
        "merged_cells": [],
        "evidence": { "page_no": 3, "span": null, "source": "page_markdown" }
      }
    ],
    "figures": []
  },
  "generated_at": "2026-03-14T10:22:07Z"
}

Tables are extracted deterministically (GFM pipe-tables and raw HTML <table>s parsed from each page's markdown) — this layer does not depend on an LLM call. Large documents are capped at 300 tables per document / 500 rows per table; if a document hits that limit, the response is marked "truncated": true.

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404objects_not_availableDocument exists but structured objects haven't been generated yet

Line items (invoice / customs line items)

GET /v1/documents/{document_id}/line-items

Instead of re-parsing the raw table cells from the Objects endpoint yourself (a common need for workflows like customs declaration automation), this endpoint returns a typed, deduplicated list of invoice/customs line items.

The primary extraction path reads each page's text directly and asks a model for typed line items — no table detection required, so pages where a line-item table wasn't recognized as a table (but the items are still present as text) are no longer silently missed. Every returned item is verified against the source page text (its code and total must actually appear on that page, in some normalized form) before being accepted; anything that can't be verified is dropped and counted in dropped_by_verification, and a page whose request fails outright is listed in pages_failed — nothing is lost silently. Only when this path finds zero verified items (feature disabled, pages unavailable, or the request failed) does the endpoint fall back to the table-derived path: a deterministic column-name mapper over the structured_objects tables (a multilingual alias dictionary — TR/EN/IT — that recognizes mixed headers like "Article/Codice", "Quantity/Quantità", "Disc./Sconto"), with its own LLM fallback for when that layer finds zero items or the item totals don't reconcile against the document total (see below). The top-level pipeline field tells you which path produced the response ("typed", or absent for the table-derived path).

curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/line-items \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "line_items": {
    "items": [
      {
        "code": "A100",
        "external_ref": null,
        "description": "Machine part",
        "quantity": 10.0,
        "unit": "PZ",
        "unit_price": 5.0,
        "discount_pct": 10.0,
        "discount_amount": null,
        "line_total": 45.0,
        "tariff_code": null,
        "origin_country": null,
        "page_no": 3,
        "extraction_method": "typed",
        "confidence": 0.7
      }
    ],
    "item_count": 1,
    "subtotal": null,
    "discount_total": null,
    "total": 45.0,
    "currency": "EUR",
    "reconciliation": {
      "matched": true,
      "computed_total": 45.0,
      "document_total": 45.0,
      "diff": 0.0,
      "hint": "Line item total matches the document total."
    },
    "unmapped_tables": [],
    "pipeline": "typed",
    "pages_failed": [],
    "dropped_by_verification": 0
  },
  "generated_at": "2026-03-14T10:22:07Z"
}

Fields

FieldDescription
codeItem/product code — null if it couldn't be read (never fabricated)
external_refA serial/lot number or ID found glued to the code or reported separately (typed path's serial_no) — null if none
descriptionItem description, in the document's own language
quantity, unitQuantity (number) and unit (text, e.g. "PZ"/"KG")
unit_priceUnit price
discount_pct, discount_amountDiscount rate/amount — the typed path only ever fills discount_pct; the table path fills whichever the table carries and leaves the other null
line_totalThe line's extended total
tariff_code, origin_countryCustoms tariff code (HS code) and country of origin — null if not present in the source
page_noThe source page the item was found on — ALWAYS present as a field (null if genuinely unknown, never fabricated)
extraction_method"typed" (primary, page-text based), "table" (deterministic column mapping fallback), or "llm" (the table path's own fallback)
confidenceA number between 0 and 1, reflecting field-fill ratio for typed/table items; for llm items it's a fixed low constant

pipeline (top level, inside line_items) is "typed" when the primary path produced the response, and absent when the response came from the table-derived fallback path. pages_failed lists the (1-based) page numbers whose typed-extraction request failed outright (network/HTTP/parse error) — only present for the typed path, always [] when every page succeeded. dropped_by_verification counts typed items that were returned by the model but couldn't be verified against the source page text, and were therefore dropped rather than trusted.

Document-level subtotal/discount_total/total are derived from summary rows found inside the table rows themselves (multilingual labels like "Total"/"Totale"/"Subtotal") — a field that couldn't be found stays null. currency is picked by majority vote from the currency symbols/codes (€/$/EUR/USD, etc.) seen in item cells and summary rows; null if none is found. unmapped_tables lists the indices (matching the tables array in the /objects response) of tables that didn't look like line-item tables (fewer than 2 mapped core fields, including at least one of code/description) — nothing is silently dropped, you can always see which tables were skipped and why.

Number format

Line item tables can use either European ("1.234,56") or US ("1,234.56") number formatting — the format is resolved with a single, document-wide decision (not row by row), so the same document is never interpreted inconsistently.

Deduplication

The same line item could be counted twice — once from the raw table rows in /objects, once again in your own post-processing. This endpoint deduplicates items using a (code, quantity, line_total, page_no, external_ref) signature (external_ref carries a serial/lot number when the source distinguishes otherwise-identical rows — e.g. the typed path's serial_no, or an ID glued onto a product code in a table cell). If the same code repeats on a different page (e.g. a split shipment, or a legitimately repeated line), it stays as two separate items — the signature deliberately includes the page number, so this is never a blind code-only merge.

Reconciliation

Items are always returned even when reconciliation.matched is false — this endpoint never returns 422. A false value can mean one of: (a) the computed item total differs from the document total beyond a rounding tolerance (maybe an item was mis-read or missed), (b) no total label could be found in the document at all (document_total: null), or (c) no items were found at all (computed_total: null). The hint field always explains which case applies — use it client-side as a "flag for human review" signal.

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404line_items_not_availableDocument exists but structured objects haven't been generated yet (line items are derived from them)

Entities (typed graph)

GET /v1/documents/{document_id}/entities
curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/entities \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "entities": {
    "node_count": 2,
    "edge_count": 1,
    "nodes": [
      {
        "entity_id": "e1",
        "type": "company",
        "canonical_name": "Acme Logistics A.S.",
        "attributes": { "role": "shipper" },
        "evidence": { "page_no": 1 },
        "confidence": 0.6,
        "variants": [{ "name": "Acnne Logistics A.S.", "count": 1 }],
        "needs_review": true
      },
      {
        "entity_id": "e2",
        "type": "port",
        "canonical_name": "Rotterdam",
        "attributes": {},
        "evidence": { "page_no": 1 },
        "confidence": 1.0,
        "variants": [],
        "needs_review": false
      }
    ],
    "edges": [
      { "from": "e1", "to": "e2", "relation": "ships_to" }
    ]
  },
  "generated_at": "2026-03-14T10:22:11Z"
}

type is an open set (the extraction prompt uses examples like company, person, amount, date, reference, shipment, vessel, port, product, bank, but is not restricted to them). Every node carries the page it was found on when known; edges always reference entity_ids present in nodes (dangling edges are dropped at generation time).

OCR reconciliation

Scanned and photographed documents can have the same real-world entity read differently in different places — common OCR/VLM confusions like G0, 5S, 8B. Docsfra reconciles these mentions by consensus into a single canonical node instead of leaving them as separate entities: confidence (a number between 0 and 1, or null when it wasn't computed) reflects how dominant the chosen canonical form was among the document's mentions; variants lists the other surface forms that were folded into this node, each as {name, count}; and needs_review is true when confidence is low or the node lacks page provenance, flagging the node as worth a human glance. A cleanly-read, unambiguous entity simply gets confidence: 1.0, an empty variants array, and needs_review: false.

Two guardrails keep reconciliation from over-merging: values containing digits (dates, amounts, references, phone numbers) only merge when they differ purely by known OCR confusions — a real digit difference (20,560 vs 20,000) always stays two entities; and two mentions that play different roles relative to the same entity (say, an issue date and an expiry date) are never merged, no matter how similar the strings are. Each entity's evidence also carries the page number and character span where its surface form was actually found in the document — derived by searching the text, not asserted by the model; entities whose surface can't be located keep page_no: null and are flagged needs_review.

Bbox-targeted re-read

For an entity flagged needs_review, Docsfra can optionally re-read the source region directly from the page image: the entity's bounding box is cropped from the source page (with a small padding margin) and sent to a focused vision model (VLM) for a second, isolated reading — free of the surrounding page's visual noise. The result either confirms the existing reading or corrects it:

"evidence": {
  "page_no": 1,
  "reread": {
    "status": "corrected",
    "was": "Acnne Logistics A.S.",
    "now": "Acme Logistics A.S.",
    "model": "google/gemini-2.5-flash"
  }
}

When status is "corrected", the canonical reading is updated to now (the old reading is folded into variants) and needs_review is cleared. When status is "confirmed", the existing reading is kept as-is and needs_review is cleared with the confidence raised. This is an optional, best-effort feature (toggled by REREAD_ENABLED) — when it's off, or the re-read fails or returns nothing usable, the entity keeps its existing reconciliation result untouched.

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404entities_not_availableDocument exists but the entity graph hasn't been generated yet

Reasoning graph (logical dependencies)

GET /v1/documents/{document_id}/reasoning

Beyond what is in a document (entities, tables), the reasoning graph captures the logical dependencies stated within it — how one clause, obligation, or condition relates to another. Typical patterns: a payment depends on an invoice being issued, a penalty is triggered by a delay, one clause overrides another. This layer is document-internal only (it doesn't reason across multiple documents) and it is deliberately conservative: only relationships the document explicitly establishes are extracted — nothing is inferred or guessed. If a connection isn't stated in the text, it isn't added to the graph.

curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/reasoning \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "reasoning": {
    "node_count": 2,
    "edge_count": 1,
    "nodes": [
      {
        "node_id": "r1",
        "kind": "condition",
        "statement": "Delivery is not completed within 15 days of loading.",
        "entity_refs": ["e1"],
        "evidence": { "page_no": 2 }
      },
      {
        "node_id": "r2",
        "kind": "penalty",
        "statement": "A penalty of 0.1% per day of delay is applied.",
        "entity_refs": [],
        "evidence": { "page_no": 2 }
      }
    ],
    "edges": [
      { "from": "r2", "to": "r1", "relation": "depends_on" }
    ]
  },
  "generated_at": "2026-03-14T10:22:15Z",
  "prompt_version": "rsn-v1"
}

kind is one of obligation, condition, clause, deadline, penalty, right, or fact. entity_refs links a reasoning node back to entity_ids from the entity graph when the statement is about a known entity (see Entities above) — it's an empty array when there's no such link. relation is one of depends_on, requires, overrides, triggers, references, or excludes; edges always reference node_ids present in nodes (dangling edges are dropped at generation time), same guarantee as the entity graph's edges.

/extract's combined response includes this layer too, as a reasoning key alongside memory, objects, and entities — it can independently be null under the same rules as the other layers (see Extract below).

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404reasoning_not_availableDocument exists but the reasoning graph hasn't been generated yet (no representation, the reasoning_graph layer is empty, or the document predates this layer)

Layout & bounding boxes

GET /v1/documents/{document_id}/layout

Layout captures where things are on the page — for each page, its pixel dimensions and a list of typed blocks (table, text, title, image, footer, ...) with the bounding box each one occupies. This is what lets any citation ("this fact came from here") be drawn as a highlighted rectangle over the source page, instead of just a page number.

curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/layout \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "pages": [
    {
      "page": 3,
      "page_size": [1654, 2339],
      "blocks": [
        {
          "type": "title",
          "bbox": [120, 88, 980, 140],
          "text_head": "Cargo Description"
        },
        {
          "type": "table",
          "bbox": [120, 160, 1520, 640],
          "text_head": "Item | Qty | Weight (kg)"
        },
        {
          "type": "text",
          "bbox": [120, 660, 1520, 900],
          "text_head": "Total gross weight: 540 kg, packed in 3 crates."
        }
      ]
    }
  ]
}

page_size is [width, height] in pixels, and every bbox on that page ([x1, y1, x2, y2], top-left/bottom-right corners) is in that same coordinate system — divide by page_size to get normalized (0–1) coordinates if your renderer needs those instead. text_head is a short (≤80 char) preview of the block's text, useful for matching a block to a table row or a chunk without re-parsing the page. type is one of table, text, title, image, footer/header (from the underlying layout model; not an exhaustive enum).

Layout is a best-effort side layer: it comes from a separate layout-detection pass that runs alongside (not instead of) normal parsing, and it can be absent — for documents processed before this layer existed, or when the layout pass itself failed or wasn't configured for that document. Its absence never affects delivery of the base document, memory, objects, entities, or reasoning layers.

This is also why evidence.bbox / evidence.page_size are optional additions to the evidence objects you see elsewhere on this page (Objects' table evidence, Entities' and Reasoning graph's node evidence): when a chunk's source page has layout data and a layout block could be matched to it, its evidence gains "bbox": [[x1, y1, x2, y2], ...] and "page_size": [W, H] alongside the existing page_no — when no match is found (or the page has no layout), evidence keeps its plain { "page_no": ... } shape.

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404layout_not_availableDocument exists but layout hasn't been generated for it (best-effort layer — not every document has one)

Extract (combined view)

GET /v1/documents/{document_id}/extract

A single call that returns memory + objects + entities + reasoning together — useful when you want the full extraction picture without four round trips.

curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/extract \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "memory": { "title": "Bill of Lading — INV-001", "...": "..." },
  "objects": { "tables_count": 1, "figures_count": 0, "...": "..." },
  "entities": { "node_count": 2, "edge_count": 1, "...": "..." },
  "reasoning": { "node_count": 2, "edge_count": 1, "...": "..." },
  "schema_version": "0.2-ku",
  "generated": {
    "memory": { "model": "...", "prompt_version": "mem-v1" },
    "structured_objects": { "model": null },
    "entity_graph": { "model": "...", "prompt_version": "ent-v1", "resolver_version": "res-v1" },
    "reasoning_graph": { "model": "...", "prompt_version": "rsn-v1" }
  }
}

Unlike the single-layer endpoints, /extract returns 200 even if individual layers are missingmemory, objects, entities, or reasoning can each independently be null if that layer failed to generate or hasn't run yet. It only 404s when the document has no representation at all (e.g. an old document uploaded before the AIDR pipeline existed).

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404extract_not_availableDocument has no representation at all (individual layers being null does not 404 — see above)

Artifacts (the catalog)

GET /v1/documents/{document_id}/artifacts

The catalog endpoint: one call, every artifact your applications need for this document, with an available flag and a via pointer to where you'd actually fetch it. Use it to drive a UI ("what's ready for this document?") without probing every endpoint individually.

curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/artifacts \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "status": "completed",
  "representation": { "schema_version": "0.2-ku", "status": "ready" },
  "artifacts": {
    "source": { "available": true, "kind": "binary", "via": "GET /v1/jobs/{id}/source" },
    "markdown": { "available": true, "kind": "markdown", "via": "GET /v1/jobs/{id} → result.markdown_url" },
    "structured_markdown": { "available": true, "kind": "markdown", "via": "GET /v1/jobs/{id} → result.structured_url" },
    "canonical_json": { "available": true, "kind": "json", "via": "GET /v1/jobs/{id} → result.json_url" },
    "memory": { "available": true, "kind": "json", "via": "GET /v1/documents/{id}/memory" },
    "knowledge_units": { "available": true, "kind": "index", "count": 42 },
    "structured_objects": { "available": true, "kind": "json", "tables": 1, "figures": 0, "via": "GET /v1/documents/{id}/objects" },
    "entities": { "available": true, "kind": "graph", "nodes": 2, "edges": 1, "via": "GET /v1/documents/{id}/entities" },
    "extract": { "available": true, "kind": "json", "via": "GET /v1/documents/{id}/extract" },
    "embeddings": { "available": true, "kind": "vectors", "model": "text-embedding-3-small", "count": 87 },
    "search": { "available": true, "kind": "api", "via": "GET /v1/search" },
    "ask": { "available": true, "kind": "api", "via": "POST /v1/ask" }
  }
}

available always reflects the real state (a ref being set, a layer being non-empty, a count being non-zero) — it is never a static capability flag. via is informational and stays the same string regardless of availability, so you can build a link even for artifacts that aren't ready yet (just disable it while available is false).

A document that exists but isn't completed yet still returns 200 — most artifacts will simply read available: false (processing in progress). An old document with no representation also returns 200 with representation: null and the representation-dependent artifacts (memory, structured_objects, entities, extract) marked unavailable — the document itself isn't missing, only those artifacts are. A 404 only happens when the document truly doesn't exist (or belongs to another tenant).

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant

Provenance (audit trail)

GET /v1/documents/{document_id}/provenance

Every representation carries an audit trail of how it was built: which producer ran each step, which model/version it used, and a content hash for every layer it touched. Use this endpoint when you need to prove how an extracted fact came to be — compliance review, dispute resolution, or just debugging why two runs of the same document produced different output.

curl https://api.docsfra.com/v1/documents/d_9c1a2b3c4d5e/provenance \
  -H "Authorization: Bearer dip_live_a1b2c3d4e5f6..."
{
  "document_id": "d_9c1a2b3c4d5e",
  "schema_version": "0.2-ku",
  "status": "ready",
  "producers": {
    "canonical": { "producer": "parse_document", "rules_version": "canon-v1" },
    "memory": { "model": "gpt-4o-mini", "prompt_version": "mem-v1" },
    "entity_graph": { "model": "gpt-4o-mini", "prompt_version": "ent-v1", "resolver_version": "res-v1" }
  },
  "provenance": [
    { "step": "original", "hash": "sha256:1a2b3c...", "at": "2026-03-14T10:21:40Z" },
    { "step": "canonical", "producer": "parse_document", "output_hash": "sha256:4d5e6f...", "at": "2026-03-14T10:21:52Z" },
    { "step": "memory", "producer": "generate_memory", "model": "gpt-4o-mini", "prompt_version": "mem-v1", "output_hash": "sha256:7a8b9c...", "at": "2026-03-14T10:22:03Z" },
    { "step": "knowledge_units", "producer": "ai_index_document", "count": 42, "output_hash": "sha256:0d1e2f...", "at": "2026-03-14T10:22:18Z" },
    { "step": "embeddings", "producer": "embed_chunk_batch", "model": "text-embedding-3-small", "dim": 1536, "at": "2026-03-14T10:22:24Z" }
  ],
  "content_hash": "sha256:c0ffee1234...",
  "input_hash": "sha256:1a2b3c..."
}

Each provenance[] entry is append-only — steps are never rewritten in place, only added as new layers are produced. If a step re-runs (e.g. a document is re-processed), the previous representation is superseded rather than mutated, so its provenance trail stays intact and inspectable on its own. A document without a representation returns 404 provenance_not_available.

Errors

HTTPcodeDescription
401unauthorizedMissing, invalid, or revoked key
403service_not_enabledKey isn't authorized for the extract module
404not_foundDocument doesn't exist, or belongs to another tenant
404provenance_not_availableDocument exists but has no representation yet

Determinism & reproducibility

Docsfra pins the exact producer version behind every layer, so a given input always maps to a reproducible output:

  • canon-v1 — the canonicalization rules that turn per-page markdown into the single ordered document (page markers, line-ending normalization, whitespace collapsing). No LLM involved: the same pages always canonicalize to the same markdown and the same hash.
  • ctx-v2 — the chunking/context rules that produce knowledge units.
  • mem-v1 — the prompt version behind the memory (identity card) layer.
  • ent-v1 — the prompt version behind the entity graph layer.

These version strings are what you see in producers.*.rules_version / producers.*.prompt_version above (and in /extract's generated block). A representation is immutable once built: it always reflects the pinned versions active at the time it was produced. If Docsfra later improves a producer (a new mem-v2 prompt, for example), existing representations keep their original version tag — re-processing a document produces a new representation (the old one is marked superseded), never a silent in-place rewrite. content_hash is derived purely from layer content (canonical text, memory JSON, knowledge unit text, entity graph), so it changes if and only if the underlying content changes — it is not a pointer or a timestamp.

Notes

All endpoints in this section are unmetered — no credits are deducted for reading extraction results, regardless of call volume. They all share the same extract module entitlement, so a key authorized for extract can call all of them; search and ask remain separately entitled and billed (see Semantic Search and Q&A (RAG)).