# Dateien und Status

Eine **Datei** ist ein PDF, ein Bild oder eine E-Rechnung, die Sie schicken. Sie enthält einen oder mehrere **Belege**: Ein Scan von drei Rechnungen ist eine Datei mit drei Belegen.

## Eine Datei schicken

`POST /public/v1/files`, `multipart/form-data`, Berechtigung `documents:write`:

| Feld | Bedeutung |
| --- | --- |
| `file` | Pflicht. PDF, Bild (JPEG, PNG, TIFF, WebP) oder XML einer E-Rechnung (XRechnung, UBL, CII), bis 25 MB. Keine Archive in dieser Version. |
| `name` | Der Dateiname; sonst der des Uploads. |
| `external_ref` | Ihre eigene Kennung, bis 200 Zeichen (Odoo: `account.move,<id>`), kommt mit der Datei zurück. Erneut geschickte Bytes behalten die erste. |

| Antwort | Bedeutung |
| --- | --- |
| `202 Accepted` | Neue Datei, `status: "received"`; das Lesen beginnt sofort. |
| `200 OK` | Dieselben Bytes (nach SHA-256, nicht nach Namen) kamen schon: diese Datei, wie sie jetzt ist, weder neu gelesen noch gezählt. |
| `409 urn:ocost:file:kept` | Dieselben Bytes liegen dort, wo Schlüssel sie nicht sehen (der eigene Upload einer Person). Nichts ändert sich. |
| `413 urn:ocost:file:too_large`, `415 urn:ocost:file:format`, `411` | Zu groß, ein Format, das Ocost nicht liest, kein `Content-Length`. |

Jede Antwort trägt `X-Ocost-Documents-Left`, die Belege, die Ihr Tarif in diesem Monat noch hat.

```bash
curl -sS https://api.ocost.io/public/v1/files \
  -H "Authorization: Bearer $OCOST_KEY" \
  -F "file=@scan.pdf" -F "name=scan.pdf" -F "external_ref=account.move,42"
```

## Status

`GET /public/v1/files/{file_id}` (Berechtigung `documents:read`) liefert die Datei mit ihrem `status`:

| Status | Bedeutung |
| --- | --- |
| `received` | Gespeichert, noch nicht gelesen. |
| `reading` | Wird gelesen; antwortet kein Lesedienst, versucht Ocost es bis zu einen Tag lang erneut. |
| `read` | `documents` listet jeden Beleg: `id`, `kind`, `number`, `pages`. |
| `not_read` | Gespeichert, nicht gelesen; siehe `reason`. |
| `failed` | Nicht lesbar; siehe `reason`. |

| Grund | Bei | Bedeutung |
| --- | --- | --- |
| `limit` | `not_read` | Die Belege des Monats sind aufgebraucht; Ocost liest die Datei am `renews_on`. |
| `model_off` | `not_read` | Für dieses Unternehmen werden PDFs und Bilder nicht gelesen, E-Rechnungen schon. |
| `nothing_found` | `not_read` | Kein Beleg darin. |
| `too_large` | `not_read` | Zu viele Seiten oder ein zu großes Bild. |
| `damaged` | `failed` | Keine Datei, die Ocost öffnen kann. |
| `unavailable` | `failed` | Einen Tag lang antwortete kein Lesedienst; schicken Sie die Bytes erneut. |
| `failed` | `failed` | Alles andere. |
| `other` | beide | Ein Grund, den diese API noch nicht benennt. |

## Auf eine Datei warten

Fragen Sie eine halbe Minute lang alle 2 bis 3 Sekunden ab, danach alle 10 bis 30. Die meisten PDFs brauchen unter einer Minute, lange einige.

Für viele Dateien listen Sie die Änderungen: zuerst mit `updated_since` (oder ohne, für alle), dann dem Cursor nach.

```bash
curl -sS -G https://api.ocost.io/public/v1/files \
  -H "Authorization: Bearer $OCOST_KEY" \
  --data-urlencode "updated_since=2026-11-09T08:00:00Z" \
  --data-urlencode "status=read" --data-urlencode "status=not_read" \
  --data-urlencode "status=failed"
```

- Älteste Änderung zuerst, bis zu `limit` (1 bis 100, Standard 50), eine Datei einmal je Änderung.
- Solange `more` wahr ist, holen Sie mit `cursor=<next_cursor>` die nächste Seite; danach merken Sie sich den Cursor für die nächste Abfrage.
- `status` filtert erst nach dem Blättern, eine Seite kann also weniger als `limit` enthalten: Folgen Sie `more`, nicht der Anzahl.

Auf eine Datei warten und alles einsammeln, was sich seit der letzten Abfrage geändert hat:

```python
def wait_for(session, file_id, timeout=300):
    started = time.monotonic()
    while time.monotonic() - started < timeout:
        file = session.get(f"{API}/files/{file_id}", timeout=15).json()
        if file["status"] not in ("received", "reading"):
            return file
        time.sleep(3 if time.monotonic() - started < 30 else 15)
    return file
```

```javascript
// Jede seit dem gemerkten Cursor geänderte Datei (null: alle).
async function changedSince(cursor) {
  const files = [];
  for (;;) {
    const params = new URLSearchParams({ limit: '100', ...(cursor ? { cursor } : {}) });
    const page = await fetch(`${API}/files?${params}`, { headers: auth }).then((r) => r.json());
    files.push(...page.files);
    cursor = page.next_cursor ?? cursor;
    if (!page.more) return { files, cursor }; // `cursor` fürs nächste Abfragen merken
  }
}
```

## Das Original

`GET /public/v1/files/{file_id}/content` liefert die Bytes, wie sie geschickt wurden.

## Eine Datei löschen

`DELETE /public/v1/files/{file_id}` (Berechtigung `documents:write`) löscht die Datei mit ihren Belegen, allem Gelesenen, ihrem Suchindex und dem Original, sofort und endgültig.

| Antwort | Bedeutung |
| --- | --- |
| `204` | Gelöscht; danach `404`. |
| `409 urn:ocost:file:reading` | Wird gerade gelesen; später erneut versuchen. |
| `409 urn:ocost:file:linked` | Ocost braucht sie noch: eine gebuchte Rechnung (ob bezahlt, abgeglichen, exportiert oder nicht), ein Kontoauszug, eine offene Frage, die Datei eines Projekts. `detail` sagt, was davon; löschen Sie sie in Ocost, sobald nichts sie mehr braucht. |
