Ownware
Home›Extracta›API
Extracta · API

Extracta API, as shipped in the download

Extracta REST API & Webhooks (v2.0)

Extracta ships a JSON REST API and signed webhooks so extracted invoice data can flow into your accounting system, a spreadsheet job, or whatever you already run — without anyone re-typing it.

Extracta uses your own LLM API key. Extracted data stays on your server; each document goes only to the provider you configure (with a local Ollama model, it never leaves your server).

Authentication

Create a key in API & Webhooks (in the sidebar). Send it on every call:

Authorization: Bearer apk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.

Endpoints

Method & pathWhat it does
GET /api/pingVerify a key
GET /api/documents?status=&needs_review=1List documents
GET /api/documents/{id}One document with fields and line items
POST /api/documents/{id}/reviewApprove, optionally correcting fields first
GET /api/export?from=&to=&reviewed=1Extracted records in an invoice-date range
GET /api/openapi.jsonOpenAPI 3 spec (no auth)

Why upload is not in the API

Uploading is deliberately UI-only. A document arrives as bytes from a scanner, a phone camera or a mail rule, and it then needs a human to glance at the result. What an accounting system actually wants to talk to is everything after that — and that is what the API exposes. If you need unattended ingestion, drop files into the upload screen's watch folder and let the API handle the rest of the pipeline.

"Needs review" is an arithmetic fact, not a guess

needs_review is not a confidence score invented for the API. It is Extraction::reconcile()'s verdict — the same check the review screen runs:

  • do the line items sum to the subtotal (or total)?
  • does subtotal + tax equal the total?

If either fails, needs_review is true and reconcile_warnings says exactly what did not add up. The API, the review screen and the email alert therefore can never disagree about whether a document is trustworthy. This matters more than a failed extraction: a failure is obvious, whereas a document that extracted "successfully" with numbers that do not reconcile will go into the books wrong unless somebody looks.

GET /api/documents?needs_review=1

is the endpoint to poll — extracted, not yet approved, and internally inconsistent.

Approve or correct

curl -X POST https://your-install/api/documents/42/review \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"fields": {"total": "1,234.56", "invoice_date": "07/03/2026"}}'

Only the fields you send are touched. Every value runs through the **same normalisers the review form uses** — money parsing (US and EU formats, symbols, negatives), date normalisation honouring your day-first setting, and currency-code resolution — so a value posted here is stored exactly as the screen would have stored it. Send {"reviewed": false} to save corrections without approving.

Approving promotes the document to the reviewed status, identically to the screen.

Export

GET /api/export filters on the invoice date, not the upload date, because a bookkeeper closing July wants July's invoices however late they were scanned. reviewed=1 restricts it to documents a human has approved. Reviewed documents are always included — they are the ones most worth exporting.

Money is returned as both integer cents and the app's own decimal string, so nothing has to be re-derived downstream.

MCP — the agent surface (new in 3.0)

Extracta speaks MCP at POST /mcp, authenticated with the API key you already have:

Authorization: Bearer <your api key>

GET /mcp answers 405 with Allow: POST.

ToolWrites?What it does
list_documentsnoDocuments with status and reviewed flag
document_detailnoOne document, its fields and the arithmetic check
submit_documentYESRecords extracted values (whitelisted) and marks it reviewed
review_decideYESApprove as-is, or send back to the queue
export_rowsnoThe extracted rows, ready for a ledger

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http extracta https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "extracta",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "extracta", "name": "Extracta", "url": "https://your-install/mcp",
      "token": "apk_xxxx", "enabled": true }
  ]
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Extracta implements the protocol, not an integration with a particular vendor.

The whitelist is the product, and it applies to agents too

Extracta declares exactly eight fields — vendor, invoice_number, invoice_date, due_date, currency, subtotal_cents, tax_cents, total_cents — plus line items. **Anything else you send is dropped, not stored.** submit_document runs your fields object through the same builder the review form uses, which names every field it reads rather than copying keys, so an extra key has nowhere to go:

"fields": {"vendor": "Acme Ltd", "total": "120.00", "currency": "€",
           "bank_account": "IE29 …", "internal_note": "call the CFO"}

→ stored: vendor, invoice_number, invoice_date, due_date, currency (EUR),
          subtotal_cents, tax_cents, total_cents (12000), line_items
→ dropped: bank_account, internal_note

The response echoes stored_fields so you can see exactly what was kept. Line items narrow the same way: description, qty, unit_cents, amount_cents, nothing else.

Values also go through the product's own parsers, so "€" becomes EUR and "120.00" becomes 12000 cents — the same normalisation a person typing into the review screen gets.

Reject does not delete

review_decide with "reject" clears the reviewed flag and returns the document to the queue. The extracted values are left alone — rejecting means "a person should look at this", not "throw the work away".

Arithmetic

document_detail returns reconcile, the product's own cross-check (line items sum to the subtotal; subtotal + tax equals the total). The API reports that verdict; it never re-adds anything itself.

Extraction templates (new in 3.0)

A template is a named subset of the declared fields for a document type — a utility bill has no line items, so its template hides them. A template can only ever narrow the set: the choices are exactly the declared fields, and the intersection is applied again when the template is read, so even a hand-edited row cannot introduce a field the extractor does not know.

Erasure (new in 3.0)

Extracta has no person table — a subject appears as a value inside an extracted record. So Subject search looks across the declared fields (case-insensitively, the source documents themselves are not searched) and Redact blanks the matching text values in place.

The amounts, the dates and the source document stay. The transaction did happen, and erasing its arithmetic would falsify the books rather than protect anyone. The audit entry is a count, and the search term itself is never written to it.

Webhooks

Add receiver URLs in API & Webhooks. Events:

  • document.extracted — fields have been pulled (payload carries needs_review)
  • document.reviewed — a human approved it

Both fire from every path, including the public-demo simulation and the real LLM call, so a receiver sees the same stream regardless of how the extraction ran.

Each delivery is an HTTP POST with a JSON body and:

X-Extracta-Event: document.extracted
X-Extracta-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret>

Verify the signature before trusting a payload. Delivery is one attempt with a short timeout; the Recent deliveries log shows every attempt and its response code. Design receivers to be idempotent.

Email alerts

Set an SMTP server and a notification address in Settings. You get a mail when an extraction succeeds but does not reconcile — the case that would otherwise pass silently.

Backup

/backup.json exports every table with both the SMTP password and your LLM API key redacted. Extracta is the one product here that stores a third-party credential, and a backup file is exactly the sort of thing that ends up somewhere it should not.

Limits, honestly

  • The API acts at user level; there is no separate scope system.
  • List endpoints cap at 200 rows.
  • One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
  • Upload and re-extraction stay in the UI (see above).
  • Extraction quality is your model's, not Extracta's. The reconcile check is the safety net, and it catches arithmetic — not a plausible-looking wrong vendor name.

← Back to Extracta · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →