bimetricsEntwickler

Beleg-Upload

Belege hochladen, ohne Doppelungen wiederholen und den Verarbeitungsstatus abfragen.

Als Markdown ansehen

Achtung

Ändert Daten der Firma; wird atomar mit Idempotenzbeleg und Herkunft protokolliert.

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).
  • POST an POST /api/document/ als multipart/form-data.
  • Genau eine Datei je Anfrage, in genau einem dieser Felder:
FeldBedeutung
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 409 und dem Code UploadRequestConflict. 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:

StatusBedeutung
pendingBeleg gespeichert, Verarbeitung noch nicht gestartet
processingVerarbeitung läuft
successVerarbeitung abgeschlossen, die erkannten Werte stehen im Beleg
failedVerarbeitung fehlgeschlagen, der Beleg bleibt gespeichert
quota_pausedBeleg 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.

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.

Auf dieser Seite