Skip to content

Submit invoices

There are two ways to submit an invoice: upload the document and let Invogi read it, or send structured data you already have.

Supported files: PDF (including Factur-X/ZUGFeRD), XML (UBL, CII, XRechnung, KSeF), PNG, JPEG and TIFF.

Uploading takes three requests: reserve an upload, send the bytes, then analyze. This script does all three (it uses jq to read the JSON responses):

Terminal window
FILE=invoice.pdf
# 1. Reserve an upload. mediaType must match the file type.
UPLOAD_ID=$(curl -s https://api.invogi.com/v1/uploads \
-H "Authorization: Bearer $INVOGI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"mediaType\": \"application/pdf\", \"byteSize\": $(wc -c < "$FILE")}" \
| jq -r .upload_id)
# 2. Send the file's bytes.
curl -s -X PUT "https://api.invogi.com/v1/uploads/$UPLOAD_ID/content" \
-H "Authorization: Bearer $INVOGI_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$FILE"
# 3. Analyze it. purchase_order_ref and company_id are optional.
curl -s https://api.invogi.com/v1/invoices/from-upload \
-H "Authorization: Bearer $INVOGI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"upload_id\": \"$UPLOAD_ID\", \"purchase_order_ref\": \"PO-42\"}"

Step 3 waits for the analysis and returns the result:

{ "id": "3f2c…", "status": "COMPLETE", "decision": "PASS", "risk_score": 4 }

Good to know:

  • mediaType values: application/pdf, application/xml, text/xml, image/png, image/jpeg, image/tiff.
  • Send the bytes before the reservation’s expires_at.
  • An upload can be written only once. To replace a file, reserve a new upload.
  • Each upload can be analyzed once. A second request returns 409 CONFLICT; if analysis failed on our side, retrying the same upload is allowed.

For large documents or batch jobs, add Prefer: respond-async. When the request is queued the response is {upload_id, status: "QUEUED"} with a Preference-Applied: respond-async header. Then either wait for the invoice.analysis.completed webhook or poll GET /v1/uploads/{upload_id}/status until it reports COMPLETE (with invoice_id) or FAILED.

If the response has no Preference-Applied header, the analysis ran synchronously and the body already holds the result.

If your system already has the invoice fields, skip document reading:

Terminal window
curl https://api.invogi.com/v1/invoices \
-H "Authorization: Bearer $INVOGI_API_KEY" \
-H "Idempotency-Key: erp-bill-48213" \
-H "Content-Type: application/json" \
-d '{
"supplier": {
"name": "ACME GmbH",
"tax_id": "DE123456789",
"bank_account": {"iban": "DE89370400440532013000"}
},
"invoice_number": "INV-2026-1001",
"issue_date": "2026-09-14",
"currency": "EUR",
"net_amount": "1000.00",
"tax_amount": "190.00",
"gross_amount": "1190.00",
"purchase_order_ref": "PO-42"
}'

The response (202) already contains the decision and findings:

{
"id": "3f2c…",
"status": "ANALYZING",
"decision": "REVIEW",
"risk_score": 42,
"findings": [{ "type": "FUZZY_DUPLICATE", "severity": "MEDIUM", "...": "…" }],
"versions": { "...": "…" }
}

For UK accounts, send {"sort_code": "60-16-13", "account_number": "31926819"} instead of an IBAN.

Which company? Pass company_id (from GET /v1/companies), or a buyer: {name, tax_id} to route by VAT ID. With neither, the invoice goes to the default company. See Multiple companies.

  • GET /v1/invoices/{id}: supplier, latest decision, lines and all findings with their evidence.
  • GET /v1/invoices/{id}/analysis/latest: the latest analysis run.
  • GET /v1/invoices?decision=BLOCK: list invoices, filtered by decision, severity, supplier_id, company_id, finding_type, q, from, to.

GET /v1/invoices/{id}/export?format=json (or csv) returns the invoice in Invogi’s stable invogi.canonical-invoice.v1 format for your payment or accounting system. It is only available once the invoice’s decision is PASS.