Docsfra
API Documentation
v1

Polling

GET /v1/jobs/{id} is the only endpoint — it returns both the job status and (once complete) the result in the same response; there is no separate "get result" endpoint. Even if webhook delivery fails or never arrives, the client can always rely on this endpoint: polling is the reliable fallback for webhooks.

State machine

queued -> processing -> parsing -> indexing -> completed / failed
StateMeaning
queuedDocument received, waiting in queue.
processingJob started; the document is being split into pages (the first step of the page-by-page resilient pipeline).
parsingPages are being extracted one by one. Even if a page temporarily fails, the job does not get stuck — automatic retry and last-resort fallback are in effect; the client doesn't need to do anything.
indexingAll pages are merged in page_no order to produce the final markdown/JSON (the full-text search index is also written at this step).
completedThe result is ready; the result field is populated.
failedThe job has permanently failed; the error field is populated, result is always null.

The result field is only populated when status: "completed". The error field is only populated when status: "failed".

Once a document becomes completed, it automatically enters AI-indexing (chunk -> embed -> vector) as well as AI-representation production (memory, entities, reasoning, layout — see Extraction & Artifacts); this proceeds in the background, independently of completed. Indexed documents become queryable via Semantic Search and Q&A.

Progress of this background phase is exposed via the additive ai_status field on this same response (see below) — a job can read status: "completed" while ai_status is still chunking or embedding.

GET /v1/jobs/{id} — status and result in the same response

{
  "id": "d_9c1a...",
  "batch_id": "b_5f2a...",
  "external_item_ref": "INV-001",
  "external_batch_ref": "PO-2026-001",
  "status": "completed",
  "page_count": 12,
  "partial": false,
  "file_size_bytes": 245678,
  "source": {
    "filename": "invoice-001.pdf",
    "content_type": "application/pdf",
    "size_bytes": 245678,
    "url": "https://media-cdn.../signed?exp=..."
  },
  "result": {
    "markdown_url": "https://markdown-cdn.../result.md?signed&exp=...",
    "json_url": "https://markdown-cdn.../result.json?signed&exp=...",
    "structured_url": "https://markdown-cdn.../result.structured.md?signed&exp=..."
  },
  "error": null,
  "created_at": "2026-07-01T09:00:00Z",
  "updated_at": "2026-07-01T09:02:15Z",
  "completed_at": "2026-07-01T09:02:15Z",
  "ai_status": "ready",
  "cost": "16.6893"
}
  • ai_statusadditive field tracking the background AI-representation pipeline for this document, independent of status. One of none (not yet started, or briefly right after completed), chunking, embedding, ready (all requested AI artifacts finished), partial (some artifacts failed to produce — the rest are still usable), or failed. Poll GET /v1/documents/{id}/artifacts (see Extraction & Artifacts) to see exactly which artifacts are available.
  • source.url — the download link for the originally uploaded file. If the raw file is deleted from media-cdn after the retention period ends, url returns null (the file no longer exists, but the record and result are unaffected).
  • result.markdown_url, result.json_urltime-limited signed links, freshly generated on every GET call (lazy signing). Do not cache the link, it expires.
  • result.structured_urloptional; the raw markdown reorganized (structured) according to a fixed template. Only present when it can be generated — it's a best-effort enrichment, not a core deliverable; if it cannot be generated, the field is entirely absent from the response.

Error: 404 not_found — the id doesn't exist or belongs to another tenant (the resource's existence is never leaked via 403).

Result content: markdown, JSON, and page citations

The two (optional third) links in the result field download different bodies:

  • markdown_url — plain-text markdown with all pages merged together.

  • json_url — the DIP canonical, versioned wrapper schema. Per-page citations (pages[]) are inside this body — not in the job status response:

    {
      "schema_version": "1.0",
      "source": { "engine": "vlm", "engine_version": "per-page", "backend": "vlm-page" },
      "document": {
        "page_count": 12,
        "pages": [
          { "page": 1, "markdown": "...", "blocks": [] },
          { "page": 2, "markdown": "...", "blocks": [] }
        ],
        "metadata": {}
      },
      "markdown": "...",
      "extracted_at": "2026-07-01T09:02:15Z"
    }
    

    Each pages[] element carries which page it came from (page) and that page's markdown — use this body for page-level citation/source display.

  • structured_url (if present) — the same content reorganized according to a fixed template (Parties / Main Table / Total / Other); it is not a summary, no data from the source is dropped.

partial — partial success

In the page-by-page resilient pipeline, a temporary issue on a single page does not make the whole document failed: failed pages are automatically retried, falling back to an alternative engine as a last resort. However, if a page is exhausted across all engines:

  • The document still becomes completed (the other pages are never lost).
  • partial: true is returned — indicating that at least one page in the result is missing/a placeholder (that page's markdown in pages[] is empty).
  • partial: false means a complete result.

In flows requiring high accuracy, treat results with partial: true as "may be incomplete" and check which page is affected via pages[].

Failed job (failed)

  • error is populated, result is always null.
  • On transient errors (e.g., a storage write failure), the job is automatically retried a limited number of times in the background; the status only moves to failed once retries are exhausted.
  • failed is permanent — retrying on the client side means re-uploading the document (POST /v1/documents).

GET /v1/batches/{id} — batch status

Returns the status of a batch and all documents within it. Batch status is a derived field: if all documents are completed/failed, the batch is completed/failed; if at least one is still processing, it's processing. Each document is listed with the same short representation as GET /v1/jobs/{id} (id, external_item_ref, status); for the full result, GET /v1/jobs/{id} must still be called with the relevant document.id.

Recommended polling strategy

  • Initially check every 2-3 seconds, repeating until you reach a terminal status such as completed/failed.
  • If you get 429, honor the Retry-After header and apply exponential backoff (see the rate limit section in Getting Started & Auth). The status-query endpoint's limit is much more generous than uploads, but still avoid tight-loop polling.
  • If you're receiving Webhooks, use polling only for verification/fallback — both channels will converge to the same final state; if webhook delivery is exhausted (exhausted), the client should already be falling back to this endpoint.