DocSolved API Documentation

DocSolved provides a REST API for extracting structured data from uploaded documents or normalized inbound-email attachments. Persistent jobs, human approval, signed webhooks, and accounting exports support automation workflows in Make, n8n, Zapier, or custom integrations.

Building from a coding agent instead of application code? DocSolved MCP connects Codex, Claude Code, Gemini CLI and other MCP-compatible agents to this same API.

Extract your first document in under 3 minutes

  1. Create an API key in Developer Settings. The extract scope is enough for this loop; the key is shown once.
  2. Submit the document as an async job, poll until it finishes, fetch the result.
  3. Every field in result.extracted_fields[] has the same keys: confidence, page, evidence (the source text), match_status and bounding_box. On the OCR path they are populated when the value could be located on the page (match_status exact, fuzzy or llm_located); a value OCR could not anchor is not_found with a null box. Structured e-invoices are parsed from their XML, so their fields carry no page or box.

cURL

export DOCAI_API_KEY="sk_docai_YOUR_KEY"

# 1. submit (a retried submit with the same Idempotency-Key returns the same job)
JOB_ID=$(curl -s -X POST https://docsolved.ai/api/v1/jobs \
  -H "Authorization: Bearer $DOCAI_API_KEY" \
  -H "Idempotency-Key: invoice-2026-0847" \
  -F "[email protected]" | python3 -c 'import json,sys; print(json.load(sys.stdin)["job_id"])')

# 2. poll until status is completed / review_required / failed
until curl -s https://docsolved.ai/api/v1/jobs/$JOB_ID \
  -H "Authorization: Bearer $DOCAI_API_KEY" | grep -qE '"status": ?"(completed|review_required|failed)"'; do sleep 2; done

# 3. result
curl -s https://docsolved.ai/api/v1/jobs/$JOB_ID/result \
  -H "Authorization: Bearer $DOCAI_API_KEY"

Python

import os, time, requests

BASE = "https://docsolved.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOCAI_API_KEY']}"}

with open("invoice.pdf", "rb") as f:
    submit = requests.post(f"{BASE}/jobs", headers={**HEADERS, "Idempotency-Key": "invoice-2026-0847"},
                           files={"file": ("invoice.pdf", f, "application/pdf")}, timeout=60)
submit.raise_for_status()
job_id = submit.json()["job_id"]

while True:
    status = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30).json()["status"]
    if status in ("completed", "review_required", "failed"):
        break
    time.sleep(2)

result = requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=30).json()["result"]
for field in result["extracted_fields"]:
    print(field["field"], field["value"], field["confidence"], field.get("page"), field.get("evidence"))

JavaScript / Node.js 20+

import { readFile } from "node:fs/promises";

const BASE = "https://docsolved.ai/api/v1";
const headers = { Authorization: `Bearer ${process.env.DOCAI_API_KEY}` };

const form = new FormData();
form.append("file", new Blob([await readFile("invoice.pdf")], { type: "application/pdf" }), "invoice.pdf");
const submit = await fetch(`${BASE}/jobs`, { method: "POST", headers: { ...headers, "Idempotency-Key": "invoice-2026-0847" }, body: form });
const { job_id } = await submit.json();

let status;
do {
  await new Promise((r) => setTimeout(r, 2000));
  ({ status } = await (await fetch(`${BASE}/jobs/${job_id}`, { headers })).json());
} while (!["completed", "review_required", "failed"].includes(status));

const { result } = await (await fetch(`${BASE}/jobs/${job_id}/result`, { headers })).json();
for (const f of result.extracted_fields) console.log(f.field, f.value, f.confidence, f.page, f.evidence);

Then: webhooks instead of polling, idempotent submits, review and approval, exports, email ingestion, or the same API from a coding agent via MCP.

Authentication

Create and manage API keys in the Developer Settings page. Send the key as a bearer token; scopes and the key's monthly page quota are enforced on every request.

Authorization: Bearer sk_docai_<your_key>

API keys start with sk_docai_. Keep them secret. They are shown only once at creation.

Use the extract scope to submit jobs and read their results. Add the history scope to list saved records and download accounting exports, and the webhooks scope to manage webhook endpoints. These are the only supported API-key scopes. Keys do not expire; revoke a key from Developer Settings the moment it is no longer needed.

Rate Limits

Analysis usage is governed by your plan's page allowance and storage quota. There is no separate per-request or per-day cap.

API keys additionally meter OCR pages against the key's monthly page quota.

Requests that run out of pages or storage return HTTP 402; an API key over its monthly page quota returns HTTP 429 with a Retry-After hint.

Auxiliary endpoints (validation helpers, webhook create/test/replay, public share links) have a burst backstop of 60 requests per minute per IP, 600 for authenticated callers; every 429 carries Retry-After in seconds. The live numbers are published by GET /api/v1/capabilities under rate_limits.

Error Format

{
  "detail": "File too large. Maximum upload size is 15 MB.",
  "request_id": "3f1c9d2e8b4a4c0e9a7d1b2c3d4e5f60",
  "error": {
    "code": "FILE_TOO_LARGE",
    "message": "File too large. Maximum upload size is 15 MB.",
    "retryable": false,
    "request_id": "3f1c9d2e8b4a4c0e9a7d1b2c3d4e5f60"
  }
}

Switch on error.code, never on the text. Codes: INVALID_REQUEST, AUTHENTICATION_REQUIRED, PERMISSION_DENIED, NOT_FOUND, FILE_TOO_LARGE, UNSUPPORTED_DOCUMENT, RATE_LIMITED, QUOTA_EXCEEDED, IDEMPOTENCY_KEY_CONFLICT, PROCESSING_TIMEOUT, INTERNAL_ERROR. Retry only when retryable is true, honouring Retry-After, and resend the same Idempotency-Key. Quote request_id (also sent as the X-Request-ID header) when contacting support.

Compatibility

Inside /api/v1 changes are additive only: paths, fields, error codes and job statuses are never removed or retyped. Breaking changes ship as /api/v2 with at least 12 months of overlap, announced on the changelog and with Deprecation and Sunset headers. Read GET /api/v1/capabilities at startup rather than hardcoding formats and limits.

Sync Document Extraction

POST/api/v1/analyze

Upload a document and get extraction results synchronously. Best for small documents (< 2 pages).

Request

Multipart form data:

FieldTypeDescription
filefilerequiredPDF, PNG, JPG, HEIC, AVIF, TIFF, BMP, or WEBP (max 15MB)
filenamestringoptionalOverride the displayed filename

Example

curl -X POST https://docsolved.ai/api/v1/analyze \
  -H "Authorization: Bearer sk_docai_..." \
  -F "file=@purchase_order.pdf"

Response

{
  "request_id": "3fa8...",
  "filename": "purchase_order.pdf",
  "document_type": "purchase_order",
  "document_type_confidence": 0.94,
  "summary": "Purchase order from Acme Corp to Widget Co.",
  "language": "en",
  "extracted_fields": [
    {
      "field": "po_number",
      "value": "PO-2026-0042",
      "confidence": 0.97,
      "page": 1,
      "evidence": "PO-2026-0042"
    }
  ],
  "warnings": [],
  "ocr": { "pages": 1, "tokens": 312, "language": "eng+pol" },
  "llm": { "status": "success", "provider": "openai", "model": "gpt-4o-mini" }
}

Streaming Extraction (SSE)

POST/api/v1/analyze/stream

Same as sync, but streams Server-Sent Events showing processing progress. Best for real-time UIs.

Create Async Job

POST/api/v1/jobs

Upload a document and get a job ID. Process happens in the background.

curl -X POST https://docsolved.ai/api/v1/jobs \
  -H "Authorization: Bearer sk_docai_..." \
  -F "[email protected]"
{ "job_id": "abc123", "status": "queued", "request_id": "xyz..." }

Ingest Email Attachments

POST/api/v1/ingest/email

Provider-neutral inbound-email bridge. Postmark, Mailgun, SendGrid, Amazon SES, Make, n8n, or another adapter must parse the provider payload and send accepted attachments as multipart form data. DocSolved does not host a mailbox or perform provider-specific MIME parsing.

FieldTypeDescription
message_idstringrequiredStable message identifier from the provider
source_namespacestringrequiredStable provider/account scope, such as postmark:server-123
filesfile[]requiredRepeat for every supported attachment
organization_idstringoptionalTarget workspace; the caller must have upload permission
curl -X POST https://docsolved.ai/api/v1/ingest/email \
  -H "Authorization: Bearer sk_docai_..." \
  -F "message_id=provider-message-42" \
  -F "source_namespace=postmark:server-123" \
  -F "organization_id=org_..." \
  -F "[email protected];type=application/pdf" \
  -F "[email protected];type=image/jpeg"
{
  "message_id": "provider-message-42",
  "source_namespace": "postmark:server-123",
  "accepted_count": 1,
  "duplicate_count": 1,
  "jobs": [
    {"job_id": "job-new", "status": "queued", "filename": "invoice.pdf", "duplicate": false},
    {"job_id": "job-old", "status": "completed", "filename": "receipt.jpg", "duplicate": true}
  ]
}

Idempotency includes the authenticated owner, provider/account namespace, destination, message ID, normalized filename, file content, and occurrence among identical attachments. Reordered provider retries return the original job IDs. A failed ingestion can requeue the same job with fresh bytes up to three total attempts; active and completed jobs are never processed twice.

Get Job Status

GET/api/v1/jobs/{job_id}

curl https://docsolved.ai/api/v1/jobs/abc123 \
  -H "Authorization: Bearer sk_docai_..."
{
  "id": "abc123",
  "status": "completed",
  "progress_pct": 100,
  "document_type": "invoice",
  "pages_processed": 2
}

Possible statuses: queued, processing, review_required, completed, failed, cancelled.

Get Job Result

GET/api/v1/jobs/{job_id}/result

Returns the full extraction result once job status is completed or review_required. Accepts either the extract or the history scope, so an extract-only key covers the whole submit, poll, result loop.

curl https://docsolved.ai/api/v1/jobs/abc123/result \
  -H "Authorization: Bearer sk_docai_..."

Export JSON

POST/api/v1/export/json

Send extraction result as JSON body, get clean export file.

curl -X POST https://docsolved.ai/api/v1/export/json \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_docai_..." \
  -d '{"request_id":"...","extracted_fields":[...]}' \
  -o export.json

Export CSV

POST/api/v1/export/csv

curl -X POST https://docsolved.ai/api/v1/export/csv \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_docai_..." \
  -d '{"request_id":"...","extracted_fields":[...]}' \
  -o fields.csv

Export XLSX

POST/api/v1/export/xlsx

Returns an Excel workbook with Summary, Fields, Line Items, Warnings, and Metadata sheets.

List History

GET/api/v1/history

curl https://docsolved.ai/api/v1/history \
  -H "Authorization: Bearer sk_docai_..."

Review and Approval Workflow

POST/api/v1/history/{record_id}/workflow

Apply a controlled workflow transition to a saved record. This route requires an interactive Clerk session: API keys cannot approve documents. Workspace members can review; workspace admins can approve, request changes, or reject. A personal-record owner performs both roles.

{
  "action": "approve",
  "comment": "Ready to post"
}

Actions: start_review, complete_review, reopen_review, submit_for_approval, approve, request_changes, reject, and accept_partial_analysis. Approval commits the workflow audit event and document.approved outbox event together.

Delete All History

DELETE/api/v1/history

Permanently deletes all saved analyses for your account.

List API Keys

GET/api/v1/keys

Create API Key

POST/api/v1/keys

curl -X POST https://docsolved.ai/api/v1/keys \
  -H "Authorization: Bearer <clerk_session_token>" \
  -H "Content-Type: application/json" \
          -d '{"name": "My automation key", "scopes": "extract,history"}'
{
  "id": "...",
  "name": "My automation key",
  "key": "sk_docai_...",   // shown ONCE - save it now
  "prefix": "sk_docai_abc",
  "scopes": "extract,history",
  "created_at": "2026-06-10T12:00:00Z"
}

Revoke API Key

DELETE/api/v1/keys/{key_id}

Create Webhook

POST/api/v1/webhooks

curl -X POST https://docsolved.ai/api/v1/webhooks \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
          -d '{"name": "My webhook", "endpoint_url": "https://my.app/hook", "event_types": ["document.completed", "document.review_required", "document.approved", "document.failed"]}'

The response includes a secret for signature verification. Save it now, it won't be shown again.

Webhook Event Types

EventWhen it fires
document.completedExtraction completed and the result was saved
document.review_requiredValidation or confidence routing requires human review; also fires with document.completed
document.approvedAn authorized approver committed the approval decision; use this for accounting writes
document.failedProcessing failed; email-ingested jobs include retryability and attempt metadata
Use document.approved, not document.completed, as the control point for posting data to an accounting system. Extraction can complete while fields still require correction.

Test Webhook

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

Verify Webhook Signature

Every delivery includes these headers:

import hashlib, hmac

def verify(secret, body_str, timestamp, signature_header):
    msg = f"{timestamp}.{body_str}"
    digest = hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sha256={digest}", signature_header)

Failed Durable Events

DocSolved retries outbound deliveries automatically. If a durable outbox event cannot be expanded after five attempts, it becomes a dead letter. The owner can inspect metadata without exposing the stored payload and replay the event after correcting the cause.

GET/api/v1/webhook-outbox/failed

curl https://docsolved.ai/api/v1/webhook-outbox/failed \
  -H "Authorization: Bearer sk_docai_..."

POST/api/v1/webhook-outbox/{event_id}/replay

curl -X POST https://docsolved.ai/api/v1/webhook-outbox/EVENT_ID/replay \
  -H "Authorization: Bearer sk_docai_..."

The same controls are available under Developer Settings β†’ Events needing attention. Delivery and terminal outbox records are retained for 30 days.

Use with Make / n8n / Zapier

For direct uploads, call /api/v1/jobs. For inbound email, let the provider parse attachments and call /api/v1/ingest/email with a stable message ID and source namespace. Both routes create the same persistent jobs and use the same review workflow.

Subscribe to completion and review events for status, then use document.approved to trigger controlled downstream accounting actions. Signed webhooks remove the need to poll.

Frequently asked questions

How do I authenticate with the DocSolved API?

Create an API key at /developer β€” it is shown only once β€” then send it as a Bearer token in the Authorization header: Authorization: Bearer sk_docai_your_key.

What is the difference between /api/v1/analyze and /api/v1/jobs?

/api/v1/analyze returns the result in the same request, which suits interactive use. /api/v1/jobs queues the document so you poll the job or receive a signed webhook when it finishes β€” better for batches and large documents.

How do I get notified when a document is finished?

Register a webhook. DocSolved POSTs an HMAC-SHA256-signed event on document.completed, document.review_required, document.failed and document.approved. Verify the X-DocAI-Signature header and return HTTP 2xx within five seconds.

Does the API return source evidence for extracted values?

For scanned documents, each field carries the text it was read from and, when OCR located the value, its page and bounding box. Structured e-invoices are parsed from their XML and carry no bounding box by design.