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}/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

Auth and entitlement

All six endpoints 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

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