# Beleg hochladen und auslesen (https://developer.bimetrics.de/recipes/document-upload)

Einen Beleg ohne Doppelungen hochladen, den Stand der Erkennung abfragen und die erkannten Daten lesen.

> **Achtung:** Erzeugt einen echten Beleg, verbraucht Kontingent und wird GoBD-protokolliert.

Probiere das Rezept deshalb mit einer Firma und einem Beleg aus, die du dafür vorgesehen hast. Die Regeln für den Upload stehen ausführlich unter [Beleg-Upload](/document-upload).

## Ablauf

1. **Hochladen:** Die Datei geht als `multipart/form-data` an [`POST /api/document/`](https://developer.bimetrics.de/reference/documents/upload-document.md), mit einer eigenen ID im Header `X-Upload-Request-Id`. Die Antwort enthält den neuen Beleg mit seiner `id`.
2. **Stand abfragen:** [`GET /api/document/{id}`](https://developer.bimetrics.de/reference/documents/get-document.md) liefert den Beleg mit dem Stand der Erkennung in `uploadDocument.ocrStatus`. Du fragst alle paar Sekunden ab, bis der Stand `success`, `failed` oder `quota_paused` ist.
3. **Ergebnis lesen:** Bei `success` stehen die erkannten Daten im Beleg, etwa Rechnungsnummer, Rechnungsdatum, Aussteller und Positionen.

## Code

Du brauchst einen Schlüssel mit der Berechtigung `upload` für den Upload und `read` für die Abfragen, gespeichert in der Umgebungsvariable `BIMETRICS_API_KEY`. Die Beispiele laufen mit curl und [jq](https://jqlang.org), mit Node.js ab Version 20 oder mit Python und dem Paket `requests`. Das JavaScript-Beispiel läuft nur als ES-Modul, weil es `import` und `await` außerhalb von Funktionen nutzt: Speichere es als `beleg-upload.mjs` oder setze in deiner `package.json` `"type": "module"`.

**cURL:**

```bash
#!/usr/bin/env bash
set -euo pipefail

API="https://app.bimetrics.de/api"
AUTH="Authorization: Bearer $BIMETRICS_API_KEY"
# Eine eigene ID je Beleg, zum Beispiel die Belegnummer in deinem System.
# Bei einer Wiederholung bleibt sie gleich.
UPLOAD_ID="wawi-2026-000123"

# 1. Hochladen. --retry wiederholt bei Zeitüberschreitung, 429, 500, 502,
#    503 und 504 mit derselben ID und wartet dabei die Zeit aus Retry-After ab.
id=$(curl --fail --silent --show-error --retry 5 "$API/document/" \
  -H "$AUTH" \
  -H "X-Upload-Request-Id: $UPLOAD_ID" \
  -F "auto[]=@rechnung.pdf" | jq -r '.[0].id')
echo "Beleg $id"

# 2. Stand abfragen, alle 5 Sekunden, höchstens 10 Minuten lang
status=""
for _ in $(seq 120); do
  status=$(curl --fail --silent --show-error --retry 5 "$API/document/$id" -H "$AUTH" \
    | jq -r '.uploadDocument.ocrStatus')
  case "$status" in success | failed | quota_paused) break ;; esac
  sleep 5
done
echo "Stand der Erkennung: $status"

# 3. Ergebnis lesen
if [ "$status" = "success" ]; then
  curl --fail --silent --show-error --retry 5 "$API/document/$id" -H "$AUTH" | jq '{
    kind,
    invoiceNumber,
    invoiceDate,
    issuer: .issuer.name,
    totalCents: ([.invoicePositions[]?.total] | add)
  }'
fi
```

**JavaScript:**

```js
import { readFile } from "node:fs/promises";
import { setTimeout as sleep } from "node:timers/promises";

const api = "https://app.bimetrics.de/api";
const auth = { Authorization: `Bearer ${process.env.BIMETRICS_API_KEY}` };

// Sendet eine Anfrage und wiederholt sie bei Verbindungsfehlern, 429 und 5xx.
async function call(path, init = {}, attempts = 5) {
  for (let attempt = 1; ; attempt++) {
    let response;
    try {
      response = await fetch(api + path, { ...init, headers: { ...auth, ...init.headers } });
    } catch (error) {
      if (attempt >= attempts) throw error;
      await sleep(2 ** attempt * 1000);
      continue;
    }
    if ((response.status === 429 || response.status >= 500) && attempt < attempts) {
      const seconds = Number(response.headers.get("Retry-After")) || 2 ** attempt;
      await sleep(seconds * 1000);
      continue;
    }
    if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
    return response.json();
  }
}

// 1. Hochladen. Kein eigener Content-Type: fetch setzt ihn für FormData selbst.
const form = new FormData();
const file = new Blob([await readFile("rechnung.pdf")], { type: "application/pdf" });
form.append("auto[]", file, "rechnung.pdf");

const [upload] = await call("/document/", {
  method: "POST",
  // Eine eigene ID je Beleg; bei einer Wiederholung dieselbe.
  headers: { "X-Upload-Request-Id": "wawi-2026-000123" },
  body: form,
});
console.log("Beleg", upload.id, upload.duplicate ? "(lag schon vor)" : "");

// 2. Stand abfragen, alle 5 Sekunden, höchstens 10 Minuten lang
const done = ["success", "failed", "quota_paused"];
let document = upload;
for (let i = 0; i < 120 && !done.includes(document.uploadDocument?.ocrStatus); i++) {
  await sleep(5000);
  document = await call(`/document/${upload.id}`);
}
const status = document.uploadDocument?.ocrStatus;
console.log("Stand der Erkennung:", status);

// 3. Ergebnis lesen
if (status === "success") {
  const totalCents = (document.invoicePositions ?? []).reduce((sum, position) => sum + position.total, 0);
  console.log({
    kind: document.kind,
    invoiceNumber: document.invoiceNumber,
    invoiceDate: document.invoiceDate, // { year, month, day } oder null
    issuer: document.issuer?.name,
    totalCents,
  });
}
```

**Python:**

```python
import os
import time

import requests

API = "https://app.bimetrics.de/api"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['BIMETRICS_API_KEY']}"

def call(method, path, attempts=5, **kwargs):
    """Sendet eine Anfrage und wiederholt sie bei Verbindungsfehlern, 429 und 5xx."""
    for attempt in range(1, attempts + 1):
        try:
            response = session.request(method, API + path, timeout=120, **kwargs)
        except (requests.ConnectionError, requests.Timeout):
            if attempt == attempts:
                raise
            time.sleep(2**attempt)
            continue
        if (response.status_code == 429 or response.status_code >= 500) and attempt < attempts:
            time.sleep(int(response.headers.get("Retry-After", 2**attempt)))
            continue
        response.raise_for_status()
        return response.json()

# 1. Hochladen. Die Datei wird einmal gelesen, damit eine Wiederholung
#    genau denselben Inhalt sendet. Eine eigene ID je Beleg; bei einer
#    Wiederholung dieselbe.
with open("rechnung.pdf", "rb") as file:
    content = file.read()
[upload] = call(
    "POST",
    "/document/",
    headers={"X-Upload-Request-Id": "wawi-2026-000123"},
    files={"auto[]": ("rechnung.pdf", content, "application/pdf")},
)
print("Beleg", upload["id"], "(lag schon vor)" if upload.get("duplicate") else "")

# 2. Stand abfragen, alle 5 Sekunden, höchstens 10 Minuten lang
done = {"success", "failed", "quota_paused"}
document = upload
for _ in range(120):
    if document["uploadDocument"]["ocrStatus"] in done:
        break
    time.sleep(5)
    document = call("GET", f"/document/{upload['id']}")
status = document["uploadDocument"]["ocrStatus"]
print("Stand der Erkennung:", status)

# 3. Ergebnis lesen
if status == "success":
    print({
        "kind": document["kind"],
        "invoiceNumber": document["invoiceNumber"],
        "invoiceDate": document["invoiceDate"],  # {"year", "month", "day"} oder None
        "issuer": document["issuer"]["name"],
        "totalCents": sum(p["total"] for p in document["invoicePositions"] or []),
    })
```

## Die Upload-ID

* Verwende für jeden **neuen** Beleg eine **neue** ID, am besten eine, die es in deinem System schon gibt, etwa die Belegnummer. Dann kannst du einen abgebrochenen Lauf später mit derselben ID fortsetzen.
* Bei einer **Wiederholung** bleiben ID, Datei, Dateiname und Feld gleich. Die API liefert dann denselben Beleg zurück, auch wenn die erste Anfrage schon angekommen war.
* Dieselbe ID mit einer anderen Datei ergibt `409` mit dem Code `UploadRequestConflict`. Das wiederholst du nicht.

Liegt dieselbe Datei in der Firma schon als Beleg vor, legt bimetrics keinen zweiten an. Die Antwort enthält dann den vorhandenen Beleg und `"duplicate": true`.

## Stand der Erkennung

| `uploadDocument.ocrStatus` | Was tun                                                                                                                                                                                      |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`, `processing`    | Weiter abfragen. Nach einem vorübergehenden Fehler kann der Stand von `processing` wieder auf `pending` gehen.                                                                               |
| `success`                  | Ergebnis lesen.                                                                                                                                                                              |
| `failed`                   | Der Beleg ist gespeichert, die Daten wurden aber nicht erkannt. Prüfe ihn in der App.                                                                                                        |
| `quota_paused`             | Der Beleg ist gespeichert, das [Kontingent](/glossary#kontingent) ist ausgeschöpft. bimetrics setzt die Erkennung fort, sobald wieder Kontingent frei ist. Lade den Beleg nicht erneut hoch. |

* Frage höchstens alle paar Sekunden ab. Jede Abfrage zählt zu den [Limits](/limits).
* Bleibt der Stand länger bei `pending` oder `processing`, brich nach einer Obergrenze ab und frage später wieder ab. Der Beleg ist in jedem Fall gespeichert.
* Lädst du viele Belege hoch, frage den Stand gesammelt über [`POST /api/document/filter`](https://developer.bimetrics.de/reference/documents/filter-documents.md) ab, etwa mit `uploadDocument.ocrStatus` und `createdAt`, statt jeden Beleg einzeln.

## Das Ergebnis

Die erkannten Daten stehen direkt im Beleg (gekürzt, Werte erfunden):

```json
{
  "id": "0b5c2f4e-8a1d-4c3e-9f6a-2d7b1e8c4a90",
  "kind": "receipt",
  "invoiceNumber": "RE-2026-0917",
  "invoiceDate": { "year": 2026, "month": 9, "day": 17 },
  "dueDate": { "year": 2026, "month": 10, "day": 1 },
  "issuer": { "name": "Schreinerei Albrecht GmbH", "iban": "DE89370400440532013000" },
  "recipient": { "name": "Mustermann Bau GmbH" },
  "invoicePositions": [
    { "label": "Einbauschrank Eiche", "total": 238000, "vatPercentage": "19" },
    { "label": "Montage", "total": 35700, "vatPercentage": "19" }
  ],
  "uploadDocument": { "ocrStatus": "success" }
}
```

* `total` ist der Bruttobetrag der Position in Cent, `vatPercentage` der Steuersatz in Prozent. Der Bruttobetrag des Belegs ist die Summe der `total`-Werte, hier 2.737,00 €. Mehr unter [Datentypen](/data-types).
* Mit `auto[]` kann sich `kind` bei der Erkennung ändern, etwa auf `invoice`, wenn die Rechnung von der eigenen Firma stammt (siehe [Glossar](/glossary#beleg)).
* Die Originaldatei rufst du mit [`GET /api/document/{id}/content`](https://developer.bimetrics.de/reference/documents/get-document-content.md) ab.
