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.
Upload a document
Section titled “Upload a document”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):
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:
mediaTypevalues: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.
Analyze in the background
Section titled “Analyze in the background”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.
Send structured data
Section titled “Send structured data”If your system already has the invoice fields, skip document reading:
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.
Read the result
Section titled “Read the result”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 bydecision,severity,supplier_id,company_id,finding_type,q,from,to.
Hand off clean invoices
Section titled “Hand off clean invoices”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.