EnterDocs

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

Every amount in this API is an integer number of cents, and every field carrying one is suffixed _cents. There are no floating-point amounts anywhere in the product. An accounting system that stores 12480.10 as a binary fraction is one rounding pass away from a ledger that does not tie out.
This deployment runs in demo mode. Reads return fixture data; writes are validated, acknowledged with 202 and not persisted — every response carries "demo": true and an X-EnterDocs-Mode header. In production the workspace comes from the API key; here it comes from ?business=, which is the one parameter that would be deleted.

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

GET/v1/documents

The 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

businessstringDemo workspace id.
statestringOptional.
kindstringOptional.
pageintegerDefaults to 1.
per_pageintegerMax 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
}
GET/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
}
POST/v1/documents

Ingest, 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
}
PATCH/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.

GET/v1/findings

Sorted 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

severitystringblocking | attention | note
checkstringduplicate | 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

GET/v1/suppliers

Every 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
}
oib_valid and iban_valid run the real ISO 7064 MOD 11,10 and MOD 97 algorithms on every request rather than a length test. Measured against the fixtures in this build: 693 of 693 single-digit OIB corruptions rejected, 612 of 612 for IBAN.

Ledger

GET/v1/entries

Debit 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

GET/v1/obligations

Both 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

GET/v1/providers

The 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

capabilitystringdocument.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

POST/v1/exports

Formats: 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

StatusCodeWhen
400invalid_jsonThe body is not JSON.
404not_foundNo document, period or entry with that id.
409period_not_closeableThe period has blocking items.
422unknown_businessThe business parameter is not one of the five.
422validation_failedA 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.