Ownware
Home›Cargora›API
Cargora · API

Cargora API, as shipped in the download

Cargora REST API, MCP & Webhooks (v3.0)

Cargora ships a JSON REST API and signed webhooks so the documents you extract — and the delivery discrepancies they reveal — can reach your purchasing system, Power Automate, Zapier, n8n, or your own code, without anyone re-keying a packing slip.

The important part: discrepancies come from one engine. The short shipments, over shipments, price gaps and missing lines the API returns are computed by the same Reconcile::reconcile() the report screen renders and the CSV export writes. An integration that disagreed with the screen about what a supplier owes you would be worse than no integration at all.

Authentication

Create a key in API & Webhooks. A key belongs to a user and can do what a signed-in user can do. 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.

Money is integer cents. Quantities are numbers (they can be fractional — you buy 2.5 kg of something).

Endpoints

Method & pathWhat it does
GET /healthzLiveness check — no auth. {"ok":true,"app":"cargora","version":"3.0.0"}
GET /api/pingVerify a key
GET /api/documentsList documents (no line items — a poller should stay light). ?status= · ?type= · ?supplier= · ?q= · `?reviewed=0\1 · ?limit=`
GET /api/documents/{id}Extraction results and every discrepancy this document is involved in
POST /api/documents/{id}/reviewAccept the extraction, or correct line items
GET /api/openapi.jsonOpenAPI 3 spec — no auth

status accepts exactly the statuses the app itself uses: pending, extracted, reviewed, failed.

One document, with its discrepancies

GET /api/documents/1
{
  "document": {
    "ref_number": "PO-2026-0142", "doc_type": "po", "status": "reviewed",
    "line_item_count": 6, "total_cents": 218200,
    "reviewed_at": "2026-08-06 17:31:02", "reviewed_note": "checked against the paper slip",
    "line_items": [ {"id": 13, "sku": "NW-4471", "qty": 120, "unit_price_cents": 350,
                     "line_total_cents": 42000} ]
  },
  "reconciliations": [
    {
      "id": 2, "po": {"ref_number": "PO-2026-0142"}, "slip": {"ref_number": "SLIP-88245"},
      "summary": {"matched": 5, "qty_mismatches": 1, "price_mismatches": 1,
                  "missing_on_slip": 1, "extra_on_slip": 1, "discrepancies": 4,
                  "clean": false, "discrepant_lines": 2, "has_discrepancies": true},
      "matched": [ {"sku": "NW-4471", "po_qty": 120, "slip_qty": 90, "qty_delta": -30,
                    "qty_status": "short", "price_status": "match", "discrepancy": true} ],
      "missing_on_slip": [...], "extra_on_slip": [...]
    }
  ]
}

Reconciliations are recomputed live on every read, so a correction you make to a line item is reflected immediately — there is no stale stored report to go out of date.

summary is the engine's own, passed through untouched, plus two conveniences: discrepant_lines (matched lines with a quantity or price problem) and has_discrepancies (simply the negation of the engine's clean flag — so the API can never disagree with the screen about whether a delivery was clean).

qty_delta is slip minus PO: negative means short-shipped, positive means over-shipped.

Review — accept, or correct

curl -X POST https://your-install/api/documents/3/review \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"accept": true, "note": "checked against the paper slip",
       "line_items": [{"id": 13, "qty": 7, "unit_price_cents": 1250}]}'

Corrections apply only to the line items you name; anything omitted is left alone. If you change a quantity or a unit price without supplying line_total_cents, the line total is recomputed (7 × 1250 = 8750) so a document's total cannot drift away from the lines it is made of.

Every correction is validated before any of them is written, so a bad line cannot leave the document half-corrected. A line item belonging to a different document is refused:

422  {"error": "unprocessable", "detail": "line_items[0]: no line item 1 on this document."}

accept: true stamps reviewed_at and moves the document to reviewed — using the app's own status machine, the same one the Save button uses. That stamp is the difference between "the model said so" and "a human accepted it", which is exactly what an audit of an AI extraction needs. Send accept: false to clear the stamp and send it back for another look.

Webhooks

Add receiver URLs in API & Webhooks. Events:

  • document.extracted — an extraction completed, carrying the parsed header and line items
  • discrepancy.found — a reconciliation turned up a problem

discrepancy.found fires only when something is wrong — a short or over shipment, a price gap, a PO line missing from the slip, or a line delivered that was never ordered. A clean reconciliation sends nothing, so an alert always means something needs attention.

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

X-Cargora-Event: discrepancy.found
X-Cargora-Signature: sha256=<hex HMAC-SHA256 of the RAW body, keyed with the endpoint's secret>

Verify the signature over the raw body before trusting a payload:

$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $YOUR_SECRET);
if (!hash_equals($expected, $_SERVER['HTTP_X_CARGORA_SIGNATURE'] ?? '')) { http_response_code(401); exit; }

One attempt, 5-second timeout; the Recent deliveries log shows every attempt and response code. Design receivers to be idempotent.

MCP — the endpoint for AI agents (new in 3.0)

Cargora speaks the Model Context Protocol at POST /mcp, stateless streamable-HTTP: one JSON-RPC message in, one JSON response out. Point an MCP-capable agent at it and it can read your purchase orders, ask whether a delivery arrived in full, and correct a mis-extracted line — without you writing an integration.

Authentication is the same API key as the REST surface, and it carries the same role:

POST /mcp
Authorization: Bearer apk_...
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}

GET /mcp answers 405 with Allow: POST rather than 404, so a probing client can tell the difference between "wrong method" and "no MCP support here".

ToolReads or writesWhat it does
list_documentsREAD-ONLYPOs, packing slips and delivery notes; filter by type, status or supplier.
document_detailREAD-ONLYOne document with its line items, the per-line arithmetic check, and the reconciliations it takes part in.
line_item_updateWRITESCorrect one extracted line. Omit the line total and it is recomputed from qty × unit price, exactly as the edit form does.
match_statusREAD-ONLYWith a slip: that delivery against the order. Without one: the cumulative picture across every slip matched to the PO — ordered vs received vs still outstanding.
discrepanciesREAD-ONLYShort and over shipments, price gaps, ordered lines missing from a slip, and lines delivered that were never ordered.
export_rowsREAD-ONLYA document's line items, or a reconciliation report, through Cargora's own exporters.

Three things worth knowing:

Nothing is re-derived. match_status and discrepancies return the output of the same Reconcile:: engine the screens render. The suite asserts equality between the two rather than trusting the code to stay honest — an agent that disagreed with the receiving desk about what a supplier owes would be worse than no agent at all.

Arguments are guarded. Every argument must be a single scalar; a list or object where one is not expected is refused, unknown arguments are refused by name, and a value outside an enum is refused with the permitted values. An agent that typos an argument is told, not half-obeyed.

Roles apply identically. A key belonging to a viewer can call every read tool and is refused on line_item_update with a message that names the role.

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 cargora 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": "cargora",
  "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": "cargora", "name": "Cargora", "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: Cargora implements the protocol, not an integration with a particular vendor.

Three deliberate omissions

There is no upload endpoint. Uploading a document is multipart, and extraction behind it is long-running, spends your LLM credit per call, and is guarded by file-type checks and a demo limiter that live on the upload screen. Exposing that as a JSON endpoint would have meant either duplicating those guards or quietly weakening them, and an endpoint that can burn an operator's API budget deserves the deliberation a browser step provides. Documents arrive through the UI; everything after that — listing, reading, reviewing, correcting, and reacting to discrepancies — is fully available here.

There is no extraction tool on MCP, and that is on purpose. Running an extraction calls your LLM provider with your key and spends your credits. An agent that could trigger it could spend money in a loop, on a schedule, with nobody watching. Asking for a tool named extract returns Unknown tool. Documents are uploaded and extracted by a person; everything after that — reading, matching, correcting, exporting — is fully available to an agent.

There is no CSV import. Cargora's whole job is turning PDFs and photos of documents into structured data. A CSV of line items would already be structured data, which means it never needed Cargora. The nearest genuine equivalents already exist: the document export (/documents/{id}/export.csv and .json) to get data out, and this API to correct it in place.

Limits, honestly

  • A key carries its user's role (viewer / member / admin), so it is scoped exactly as that person is — a viewer's key cannot correct a line item, over REST or over MCP.
  • One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
  • GET /api/documents caps at 500 rows and omits line items by design; fetch a document by id when you need its contents.
  • Reconciliations are created on the Reconcile screen — pairing the right PO with the right slip is a judgement call, and getting it wrong produces a confidently wrong discrepancy report.
  • Extraction quality is your model's, not Cargora's. That is precisely why the review step, and the reviewed_at stamp, exist.

← Back to Cargora · 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 →