API reference
Every example on this page runs against this deployment. Copy one into a terminal and it returns real JSON.
Base URL
https://api.enterdocs.io/v1
Auth
X-API-Key: edo_…
Money
Integer cents, always
Workspaces
Five sample workspaces: salon, market, autoparts, restaurant, gym. An unrecognised value is rejected with 422 rather than quietly defaulted — silently returning somebody else's data is the wrong failure for an API to have.
curl "https://enterdocs.io/api/v1/overview?business=autoparts"
Documents
/v1/documentsThe queue. Filter by state (received, reading, needs_review, ready, filed, posted, rejected) and kind (invoice_in, invoice_out, delivery_note, credit_note, receipt, contract, bank_statement).
Query parameters
| business | string | Demo workspace id. |
| state | string | Optional. |
| kind | string | Optional. |
| page | integer | Defaults to 1. |
| per_page | integer | Max 100. |
Example request
curl "https://enterdocs.io/api/v1/documents?business=autoparts&state=needs_review"
Example response
{
"data": [
{
"id": "d-ad-106",
"kind": "invoice_in",
"source": "email",
"state": "needs_review",
"received_at": "2026-08-07T11:03:00Z",
"origin": "FC-2026-0812.pdf",
"supplier_id": "s-filter",
"gross_cents": 66750,
"currency": "EUR",
"pages": 1,
"arithmetic_closes": true,
"matched_document_ids": [],
"journal_entry_id": null,
"fields": { "total": 9, "settled": 9, "needs_person": 0 },
"rejection": null
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 7, "last_page": 1 },
"demo": true
}/v1/documents/{id}One document with every field's provenance: each reader that was asked including the one overruled, what settled the disagreement, and the sentence explaining it. This is the endpoint the product's argument lives in.
Example request
curl "https://enterdocs.io/api/v1/documents/d-ad-104?business=autoparts"
Example response
{
"data": {
"id": "d-ad-104",
"kind": "invoice_in",
"state": "needs_review",
"fields": [
{
"key": "gross_total",
"label": "Total due",
"value": "1.560,00",
"verdict": "agreed",
"provenance": {
"settled_by": "arithmetic",
"reasoning": "The line items plus VAT come to €1,560.00, which confirms 1.560,00 and rules out 1.580,00. The other reader saw 8 where this reads 6, a known confusion.",
"opinions": [
{ "reader_id": "google-docai", "raw": "1.560,00", "confidence": 0.94,
"box": { "page": 1, "x": 0.6, "y": 0.833, "w": 0.34, "h": 0.05 } },
{ "reader_id": "azure-di", "raw": "1.580,00", "confidence": 0.93,
"box": { "page": 1, "x": 0.6, "y": 0.833, "w": 0.34, "h": 0.05 } }
]
}
}
],
"arithmetic": {
"line_sum_cents": 124800,
"stated_net_cents": 124800,
"stated_vat_cents": 31200,
"stated_gross_cents": 156000,
"closes": true,
"discrepancy": null
},
"findings": [
{
"id": "f-drift-d-ad-104-l1",
"check": "price_drift",
"severity": "attention",
"title": "Pločice kočnice prednje, set is up 6.1% since 2026-05",
"money_at_risk_cents": 86400,
"evidence_document_ids": ["d-ad-104", "d-ad-090", "d-ad-095", "d-ad-099"]
}
]
},
"business": "autoparts",
"demo": true
}/v1/documentsIngest, multipart. Accepted and echoed in this deployment.
Example request
curl -X POST "https://enterdocs.io/api/v1/documents?business=autoparts" \ -F "file=@racun.pdf" \ -F "source=upload"
Example response
{
"accepted": true,
"applied": false,
"message": "Document ingest was accepted but not persisted — this deployment runs in demo mode.",
"demo": true
}/v1/documents/{id}Correct a field. The correction is attributable and reversible; it also feeds the supplier's posting record, which is how a rule gets demoted.
Example request
curl -X PATCH "https://enterdocs.io/api/v1/documents/d-ad-104?business=autoparts" \
-H "Content-Type: application/json" \
-d '{"fields":{"gross_total":"1.560,00"}}'Example response
{
"accepted": true,
"applied": false,
"message": "Field correction was accepted but not persisted — this deployment runs in demo mode.",
"demo": true
}Findings
Computed on every request from the workspace's own documents, never read from storage. A client polling this cannot receive a finding whose evidence does not support it, because there is nowhere for such a finding to have been written.
/v1/findingsSorted by severity, then by money at risk. Deliberately not by extraction confidence — a perfectly read bank account that changed outranks a doubtful reading of a twelve-euro line.
Query parameters
| severity | string | blocking | attention | note |
| check | string | duplicate | bank_account_changed | delivery_mismatch | price_drift | vat_rate | identifier_invalid | overdue | unmatched_delivery |
Example request
curl "https://enterdocs.io/api/v1/findings?business=autoparts&severity=blocking"
Example response
{
"data": [
{
"id": "f-iban-d-ad-106",
"check": "bank_account_changed",
"check_label": "Payment account changed",
"document_id": "d-ad-106",
"severity": "blocking",
"title": "Payment account differs from every previous invoice",
"detail": "Filter Centar d.o.o. has used HR7124840086134637292 on 24 invoices since 2022-11. This one asks for HR9223400090293874332. Both readers agree on the digits — the reading is not in doubt, the account is.",
"money_at_risk_cents": 66750,
"evidence_document_ids": ["d-ad-106"],
"resolved_at": null
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 4, "last_page": 1 },
"demo": true
}Suppliers
/v1/suppliersEvery comparison the checks make is a comparison against this collection, so it is the one to synchronise first. established_iban is the account with the history behind it — the check compares against that rather than against membership in the list, because the list accumulates the fraudulent account the moment it arrives.
Example request
curl "https://enterdocs.io/api/v1/suppliers?business=autoparts"
Example response
{
"data": [
{
"id": "s-filter",
"name": "Filter Centar d.o.o.",
"oib": "55969215684",
"oib_valid": true,
"established_iban": {
"iban": "HR7124840086134637292",
"valid": true,
"first_seen": "2022-11-02",
"document_count": 24
},
"other_ibans": [
{ "iban": "HR9223400090293874332", "valid": true,
"first_seen": "2026-08-07", "document_count": 1 }
],
"payment_term_days": 15,
"document_quality": 92,
"price_history": []
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 6, "last_page": 1 },
"demo": true
}Ledger
/v1/entriesDebit and credit totals are computed on serialisation, not stored. There is no balanced field in the data model, so an entry that does not tie out cannot be transmitted as one that does.
Example request
curl "https://enterdocs.io/api/v1/entries?business=autoparts"
Example response
{
"data": [
{
"id": "e-ad-100",
"document_id": "d-ad-100",
"date": "2026-08-01",
"posted_at": "2026-08-01T05:31:00Z",
"touched": false,
"corrected_at": null,
"lines": [
{ "account_code": "4200", "debit_cents": 140000, "credit_cents": 0, "description": "Rent" },
{ "account_code": "1600", "debit_cents": 35000, "credit_cents": 0, "description": "Input VAT" },
{ "account_code": "2200", "debit_cents": 0, "credit_cents": 175000, "description": "Trade payable" }
],
"totals": { "debit_cents": 175000, "credit_cents": 175000, "balanced": true }
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 8, "last_page": 1 },
"demo": true
}Obligations
/v1/obligationsBoth dates are returned and both countdowns are computed. The gap between them is the point: an agreement renewing on 31 December with sixty days' notice has due_at 2026-12-31 and decide_by 2026-11-01, and a client that diarises the first has diarised the wrong one. Sort and alert on days_to_decide.
Example request
curl "https://enterdocs.io/api/v1/obligations?business=gym"
Example response
{
"data": [
{
"id": "o-fp-app",
"kind": "notice_period",
"label": "Member app renews for another year on 31 December",
"due_at": "2026-12-31",
"decide_by": "2026-11-01",
"days_to_due": 143,
"days_to_decide": 83,
"amount_cents": 373500,
"document_id": "d-fp-311",
"supplier_id": "s-fp-app"
}
],
"meta": { "current_page": 1, "per_page": 25, "total": 4, "last_page": 1 },
"demo": true
}Readers
/v1/providersThe extraction engines available and which of them serve each capability. Pass ?capability= to narrow the list the way the resolver does at runtime, so a client can ask who could read this and get the answer the product would give.
Query parameters
| capability | string | document.ocr | document.classify | document.extract | document.tables | einvoice.parse |
Example request
curl "https://enterdocs.io/api/v1/providers?capability=document.extract"
Example response
{
"data": [
{
"id": "google-docai",
"name": "Document AI",
"company": "Google Cloud",
"auth": "oauth",
"api_base": "https://documentai.googleapis.com/v1",
"self_hosted": false,
"models": [
{ "id": "invoice-parser", "label": "Invoice parser",
"capabilities": ["document.classify", "document.extract", "document.tables"],
"indicative_cost_eur": 0.09, "typical_seconds": 4 }
]
}
],
"demo": true
}Exports
/v1/exportsFormats: csv, xlsx, ubl, xml. Returns 409 while the period still has blocking items — a file that silently omits four unposted documents looks complete and gets filed.
Example request
curl -X POST "https://enterdocs.io/api/v1/exports?business=autoparts" \
-H "Content-Type: application/json" \
-d '{"format":"ubl","periodId":"p-ad-2026-07"}'Example response
{
"error": {
"code": "period_not_closeable",
"message": "The period still has blocking items and cannot be exported.",
"fields": {
"ci-1": ["Documents from the period still unposted — 2"],
"ci-2": ["Invoices with an unresolved finding — 1"]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | The body is not JSON. |
| 404 | not_found | No document, period or entry with that id. |
| 409 | period_not_closeable | The period has blocking items. |
| 422 | unknown_business | The business parameter is not one of the five. |
| 422 | validation_failed | A field failed validation; see fields. |
Every list endpoint returns the same page envelope: data, then meta with current_page, per_page, total and last_page. Every response carries "demo": true while this deployment runs on fixtures.