Beleg-Upload
Belege hochladen, ohne Doppelungen wiederholen und den Verarbeitungsstatus abfragen.
Achtung
Teste den Upload deshalb mit einer Firma und einem Beleg, die du dafür vorgesehen hast. Die Referenz bietet für den Upload bewusst keinen Testversand an.
Voraussetzungen
- Ein Schlüssel mit der Berechtigung
upload(in der App: Belege hochladen). POSTanPOST /api/document/alsmultipart/form-data.- Genau eine Datei je Anfrage, in genau einem dieser Felder:
| Feld | Bedeutung |
|---|---|
auto[] | die Erkennung entscheidet die Belegart, siehe Belegart ohne Wahl |
receipt[] | Eingangsbeleg, etwa eine Rechnung eines Lieferanten |
invoice[] | Ausgangsrechnung, also eine Rechnung, die die Firma gestellt hat |
Weitere Formularfelder oder mehrere Dateien lehnt die API mit 400 ab. Unterstützt werden PDF, PNG, JPEG und Textformate wie XML-E-Rechnungen; die Art wird am Inhalt erkannt, nicht an der Dateiendung. Eine Datei darf höchstens 30 MiB groß sein; eine größere Datei ergibt 400. Ist die ganze Anfrage größer als erlaubt, antwortet die API mit 413 (siehe Limits).
Upload-ID
Jede Anfrage braucht den Header X-Upload-Request-Id: 1 bis 128 druckbare ASCII-Zeichen ohne Leerzeichen, zum Beispiel eine UUID oder die ID des Belegs in deinem System.
- Für jeden neuen Beleg eine neue ID verwenden.
- Bei einer Wiederholung derselben Übertragung ID, Datei, Dateiname und Feld gleich lassen.
curl --fail-with-body https://app.bimetrics.de/api/document/ \
-H "Authorization: Bearer $BIMETRICS_API_KEY" \
-H "X-Upload-Request-Id: rechnung-2026-0001" \
-F "auto[]=@rechnung.pdf"Sicher wiederholen
Firma, Schlüssel und Upload-ID bilden zusammen die Identität eines Uploads.
- Kommt dieselbe ID mit identischer Datei, gleichem Dateinamen und gleichem Feld erneut, liefert die API denselben Beleg zurück. Das gilt auch für parallele Anfragen. Die Wiederholung verbraucht kein weiteres Kontingent und startet keine zweite Verarbeitung.
- Kommt dieselbe ID mit anderem Inhalt, Dateinamen oder Feld, antwortet die API mit
409und dem CodeUploadRequestConflict. Das ist kein vorübergehender Fehler. - Wurde der ursprüngliche Beleg inzwischen gelöscht, legt eine Wiederholung ihn nicht neu an, sondern antwortet ebenfalls mit
409. Für einen neuen Upload brauchst du eine neue ID. - Ein anderer Schlüssel bildet einen eigenen Bereich: Dieselbe ID mit einem neuen Schlüssel ist ein neuer Upload.
Wiederhole bei Zeitüberschreitung, Verbindungsabbruch, 5xx und 429 (nach Retry-After) immer mit derselben ID.
Antwort
Die Antwort ist ein Array mit genau einem Beleg (gekürzt):
[
{
"id": "0b5c…",
"uploadDocument": { "ocrStatus": "pending" }
}
]Liegt dieselbe Datei in der Firma schon vor, legt bimetrics keinen zweiten Beleg an. Die Antwort enthält dann den vorhandenen Beleg und "duplicate": true.
Testphase
Die Testphase umfasst 100 Belege über alle Wege zusammen, also auch über die App und MCP. Ein Upload darüber hinaus antwortet mit 402 und dem Code LimitReached. detail nennt den Grund in einem Satz, context.limit und context.usage nennen Grenze und Verbrauch. Der Beleg wird nicht gespeichert. Sende ihn nach der Wahl eines Tarifs mit derselben X-Upload-Request-Id erneut.
Verarbeitung abfragen
Nach dem Upload erkennt bimetrics die Belegdaten im Hintergrund. Den Stand fragst du mit GET /api/document/{id} ab, im Feld uploadDocument.ocrStatus:
| Status | Bedeutung |
|---|---|
pending | Beleg gespeichert, Verarbeitung noch nicht gestartet |
processing | Verarbeitung läuft |
success | Verarbeitung abgeschlossen, die erkannten Werte stehen im Beleg |
failed | Verarbeitung fehlgeschlagen, der Beleg bleibt gespeichert |
quota_paused | Beleg gespeichert, Verarbeitung pausiert, weil das Belegkontingent ausgeschöpft ist |
Ist die Verarbeitung fehlgeschlagen, startest du sie mit POST /api/document/{id}/reading/retry neu, ohne die Datei noch einmal hochzuladen. Das zählt im Tarif als weiterer Beleg ins Kontingent.
bimetrics liest bis zu 60 Seiten je PDF. uploadDocument.pageCount nennt die Seitenzahl der Datei, uploadDocument.pagesRead die gelesenen Seiten. Ist pagesRead kleiner, fehlen die Seiten danach, und die Summe kann auf einer ungelesenen Seite stehen.
Die Antwort 200 auf den Upload heißt nur, dass der Beleg gespeichert ist. Die Belege warten danach der Reihe nach auf die Erkennung: Bei großen Stapeln bleibt der Status eine Weile pending, bei 100 Belegen bis zu einer halben Stunde.
Frage den Status höchstens alle 15 bis 30 Sekunden ab. Hast du mehrere Belege hochgeladen, frage nicht jeden einzeln und nicht die ganze Liste ab, sondern deine noch offenen gemeinsam, mit einem Filter an POST /api/document/filter, je Anfrage bis zu 100 IDs:
{
"kind": "and",
"children": [
{
"kind": "oneof",
"attribute": "id",
"value": ["0b5c2f4e-8a1d-4c3e-9f6a-2d7b1e8c4a90", "5e9d7c1a-3b2f-4a6e-8d0c-1f4b7a2e9c63"]
},
{ "kind": "oneof", "attribute": "uploadDocument.ocrStatus", "value": ["pending", "processing"] }
],
"limit": 100
}Fehlt einer deiner Belege in der Antwort, ist seine Verarbeitung beendet oder pausiert (success, failed oder quota_paused). Lies ihn dann einmal mit GET /api/document/{id}. Andere Änderungen, etwa an Belegen aus der App, gleichst du per Delta-Sync ab.
Bei quota_paused war der Upload erfolgreich: Lade den Beleg nicht mit einer neuen ID erneut hoch.
Ein vollständiges Beispiel in curl, JavaScript und Python, vom Upload bis zum erkannten Ergebnis, steht im Rezept Beleg hochladen und auslesen.
Belegart ohne Wahl
receipt[] und invoice[] sind bindend. Bei auto[] steht kind bis zum Ende der Erkennung auf receipt. Dann entscheidet die Erkennung mit den Daten deiner Firma, etwa Name, Steuernummern und Bankverbindung, ob die Firma den Beleg ausgestellt oder bekommen hat. Die eigene Firma legt bimetrics dabei nie als Lieferant oder Kunde an.
Kann sie nicht sicher entscheiden, bekommt der Beleg „Belegtyp prüfen“: kindReview ist true, und kind ist nur die Vermutung. Bis jemand die Belegart wählt, wird der Beleg nicht gebucht, nicht an DATEV exportiert und nicht zugeordnet. Zuordnen und Zahlung erfassen antworten mit 409 DocumentKindUnconfirmed, der DATEV-Export nennt den Beleg in Meldungen.csv.
- Finden: Filter
{ "kind": "eq", "attribute": "kindReview", "value": true }anPOST /api/document/filter. - Entscheiden:
kindinPATCH /api/document/{id}, auch mit demselben Wert. Danach bucht bimetrics den Beleg. Dreht sich die Belegart und hat seit der Erkennung niemand den Beleg bearbeitet, vergibt bimetrics die Kategorien neu. - Mehrere auf einmal:
POST /api/document/bulk/kind, siehe Mehrere Belege auf einmal.
Möglicher Doppelbeleg
Lädt dein Programm dasselbe Papier zweimal hoch, etwa als neuen Scan oder neu erzeugte PDF, findet die Upload-ID das nicht: Die Datei hat andere Bytes, also wird es ein eigener Beleg. Nach dem Auslesen nennt GET /api/document/{id} dann in possibleDuplicateOfId den älteren Beleg mit gleichem Inhalt, ebenso jeder Treffer von POST /api/document/filter.
Gleich sind dabei Belegart und Geschäftspartner (Kontakt oder Name) und außerdem
- die Rechnungsnummer (Leerzeichen und Groß-/Kleinschreibung egal), solange sich die Rechnungsdaten nicht widersprechen, bei höchstens 1 Cent Unterschied im Bruttobetrag, oder
- das Rechnungsdatum, solange sich die Rechnungsnummern nicht widersprechen, bei höchstens 1 % Unterschied im Bruttobetrag oder einer fast gleich großen Datei mit gleicher Seitenzahl ab zwei Seiten.
Eine Rechnung und eine Gutschrift über denselben Betrag sind nie Doppelbelege. Passen mehrere ältere Belege, nennt das Feld den ähnlichsten. Bei stornierten, ausgestellten und festgeschriebenen Belegen fehlt es.
Das Feld ist nur ein Hinweis: bimetrics sperrt nichts und löscht nichts, beide Belege werden gebucht. Ist es wirklich derselbe Beleg, löschst du einen der beiden mit DELETE /api/document/{id}.
Summe weicht ab
Die Erkennung liest neben den Positionen den Gesamtbetrag brutto, der auf dem Beleg steht, und nutzt ihn nur, wenn er so im Text des Belegs vorkommt. GET /api/document/{id} liefert ihn in statedTotalAmount, in Hundertsteln wie die Positionen. Weicht die Summe der Positionen um mehr als einen Cent je gelesener Zeile davon ab, liest die Erkennung sie ein zweites Mal. Bleibt die Abweichung, zeigt die App „Die Summe der Positionen weicht vom Gesamtbetrag im Beleg ab.“, der Beleg steht unter „Zu prüfen“, und Meldungen.csv im DATEV-Export nennt beide Beträge.
Gebucht wird mit den Positionen, der Export ist nicht blockiert. Korrigierst du eine verlesene Position mit PATCH /api/document/{id}, sodass die Summe stimmt, endet der Hinweis. Stimmt der Gesamtbetrag selbst nicht, etwa weil private Artikel gestrichen sind, beendet statedTotalAmount: 0 im selben PATCH den Hinweis, wie „Summe passt so“ in der App; ein anderer Wert trägt den richtigen Gesamtbetrag ein. Beides ändert keinen Betrag und geht auch bei bezahlten Belegen.
Verglichen wird ohne Vorzeichen. Bei „abzüglich 2 % Skonto“ gilt der Rechnungsbetrag vor Skonto. Zieht der Beleg Anzahlungen oder Gutscheine ab oder nennt er ein Trinkgeld, fehlt statedTotalAmount. Bei Fremdwährung endet der Vergleich, sobald der Euro-Betrag feststeht.
Steuersatz und Kategorie
Der erkannte Steuersatz bleibt erhalten, auch wenn die vorgeschlagene Kategorie ihn nicht erlaubt. Eine solche Abweichung blockiert die Buchung zur Prüfung; die Kategorie setzt den erkannten Satz nicht auf 0 %. Berichtige die Kategorie oder die Position anhand des Originalbelegs. App, REST-API und MCP prüfen dieselben Buchungsregeln.
Die Kategorie waste_disposal_expenses (Abfallgebühren, SKR03 4969 / SKR04 6859) unterstützt 0 % und 19 %. Bei 0 % ist zusätzlich der zum Original passende Steuerfall erforderlich. Bei einer Position mit 19 % kommt bestätigtes Lieferantenskonto unter den üblichen Bedingungen infrage; bei einer 0-%-Position dieser Kategorie wird es nicht angeboten und auch eine ausdrückliche Bestätigung abgelehnt. Enthält ein Beleg beide Fälle, ist kein Skonto auf den ganzen Beleg möglich. Der ursprüngliche Belegbetrag bleibt bei einem Skonto unverändert, der Ausgleich entsteht in der Zuordnung.
Die Korrektur löst keine nachträgliche Neubuchung vorhandener Belege aus. War ein gespeicherter Steuersatz bereits falsch, muss er anhand des Originals berichtigt werden.
Nachvollziehbarkeit
Jeder Upload wird im Protokoll des Belegs als Upload über einen API-Schlüssel vermerkt und dem verwendeten Schlüssel zugeordnet. Die Originaldatei rufst du mit GET /api/document/{id}/content ab.