Rezepte
Monatsexport
Die Belege eines Monats mit ihren Originaldateien und die Buchungen des Monats für ein eigenes Reporting abrufen.
Das Rezept holt für einen Monat drei Dinge: die Belege, ihre Originaldateien und die Buchungen. Ein zweiter Lauf lädt nur Dateien, die sich geändert haben.
Ablauf
- Belege des Monats:
POST /api/document/filtermitrangeaufinvoiceDate, sortiert nachinvoiceDate. Kalenderdaten schreibst du alsJJJJ-MM-TT(siehe Datentypen). - Originaldateien: Für jeden Beleg mit Datei
GET /api/document/{id}/content. DenETagder Antwort speicherst du und schickst ihn beim nächsten Lauf inIf-None-Matchmit. Ist die Datei unverändert, antwortet die API mit304ohne Inhalt. - Buchungen des Monats:
POST /api/accounting/booking/filtermitrangeaufbookingDate. Daraus bildest du etwa den Saldo je Konto.
Code
Du brauchst einen Schlüssel mit der Berechtigung read in der Umgebungsvariable BIMETRICS_API_KEY. Das Beispiel exportiert September 2026 in den Ordner export-2026-09. Das JavaScript-Beispiel braucht Node.js ab Version 20 und läuft nur als ES-Modul, weil es import und await außerhalb von Funktionen nutzt: Speichere es als monatsexport.mjs oder setze in deiner package.json "type": "module".
#!/usr/bin/env bash
set -euo pipefail
API="https://app.bimetrics.de/api"
AUTH="Authorization: Bearer $BIMETRICS_API_KEY"
FROM="2026-09-01"
TO="2026-09-30"
OUT="export-2026-09"
mkdir -p "$OUT/dateien" "$OUT/etags"
# Alle Seiten einer Filterroute als ein JSON-Array
fetch_all() { # $1 = Pfad, $2 = Filter
local offset=0 page count pages
pages=$(mktemp)
while :; do
page=$(curl --fail --silent --show-error --retry 5 "$API$1" \
-H "$AUTH" -H "Content-Type: application/json" \
-d "$(jq -c --argjson offset "$offset" '. + {limit: 1000, offset: $offset}' <<<"$2")")
echo "$page" >>"$pages"
count=$(jq '.items | length' <<<"$page")
offset=$((offset + count))
if [ "$count" -eq 0 ] || [ "$offset" -ge "$(jq .total <<<"$page")" ]; then break; fi
done
jq -s '[.[].items[]]' "$pages"
rm -f "$pages"
}
# 1. Belege mit Rechnungsdatum im Monat
documents=$(fetch_all /document/filter "$(jq -n --arg from "$FROM" --arg to "$TO" \
'{kind: "range", attribute: "invoiceDate", lower: $from, upper: $to, sort: ["invoiceDate"]}')")
echo "$documents" >"$OUT/belege.json"
# 2. Originaldateien, nur geänderte laden
for id in $(jq -r '.[] | select(.uploadDocumentId != null) | .id' <<<"$documents"); do
etag_file="$OUT/etags/$id"
etag=$(cat "$etag_file" 2>/dev/null || true)
status=$(curl --silent --retry 5 --output "$OUT/dateien/$id.tmp" --write-out '%{http_code}' \
--dump-header "$OUT/dateien/$id.headers" \
-H "$AUTH" ${etag:+-H "If-None-Match: $etag"} "$API/document/$id/content")
case "$status" in
200)
mv "$OUT/dateien/$id.tmp" "$OUT/dateien/$id"
grep -i '^etag:' "$OUT/dateien/$id.headers" | cut -d' ' -f2 | tr -d '\r' >"$etag_file"
;;
304) rm -f "$OUT/dateien/$id.tmp" ;; # unverändert
*) echo "Beleg $id: HTTP $status" >&2; exit 1 ;;
esac
rm -f "$OUT/dateien/$id.headers"
done
# 3. Buchungen des Monats und Saldo je Konto: amount auf account, -amount auf accountContra
bookings=$(fetch_all /accounting/booking/filter "$(jq -n --arg from "$FROM" --arg to "$TO" \
'{kind: "range", attribute: "bookingDate", lower: $from, upper: $to, sort: ["bookingDate"]}')")
echo "$bookings" >"$OUT/buchungen.json"
jq '[.[] | (.amount | tonumber * 100 | round) as $cents
| {model: .accountingModel, account: .account, cents: $cents},
{model: .accountingModel, account: .accountContra, cents: -$cents}]
| group_by([.model, .account])
| map({accountingModel: .[0].model, account: .[0].account, saldo: ((map(.cents) | add) / 100)})' \
<<<"$bookings" >"$OUT/salden.json"Belege
- Belege ohne Rechnungsdatum, etwa während der Erkennung, fehlen in dieser Suche. Sie kommen beim nächsten Lauf dazu, sobald das Datum erkannt ist.
- Ausgangsrechnungen und Eingangsbelege kommen gemeinsam. Mit einem zweiten Ausdruck
{ "kind": "eq", "attribute": "kind", "value": "receipt" }in einemandbeschränkst du den Export auf eine Art. - Belege aus Verträgen oder Serien haben keine Originaldatei. Ihr
uploadDocumentIdistnull, und/contentantwortet mit404. - Ein gelöschter Beleg mit festgeschriebenen Buchungen bleibt in den Treffern, mit Rechnungsdatum und Originaldatei. Seine
invoicePositionssindnull, und seine Buchungen sind per Storno aufgehoben. Über die dokumentierten Felder erkennst du solche Belege derzeit nicht sicher, der Export enthält sie deshalb mit (siehe Festschreibung).
Originaldateien
- Der
ETagist der SHA-256 der Datei als Hex-Wert, ohne Anführungszeichen. Schicke ihn genau so inIf-None-Matchzurück. Ein Wert in Anführungszeichen oder eine Liste mehrerer Werte trifft nicht. - Derselbe Wert steht auch im Beleg in
uploadDocument.blob.sha256. Stimmt er mit deinem gespeicherten Wert überein, kannst du die Anfrage ganz sparen. - Der Dateityp steht in
uploadDocument.blob.mime, der ursprüngliche Dateiname inuploadDocument.blob.originalFilename. - Jeder Abruf zählt zu den Limits. Die Beispiele warten bei
429die Zeit ausRetry-Afterab.
Buchungen
- Das Vorzeichen von
amountbezieht sich aufaccount: größer als 0 ist Soll, kleiner als 0 ist Haben. Das Gegenkonto hat die umgekehrte Seite. Deshalb zählt das Beispielamountaufaccountund-amountaufaccountContra. - Stornobuchungen zählst du einfach mit. Sie heben die stornierte Buchung auf.
- Die Suche liefert Buchungen aller Kontenrahmen, in denen die Firma gebucht hat. Das Beispiel rechnet deshalb getrennt nach
accountingModel. - Nicht festgeschriebene Buchungen ändern sich, solange sich Belege, Umsätze oder Zuordnungen ändern. Hole die Buchungen für jeden Bericht deshalb neu, statt einen alten Stand fortzuschreiben. Ob eine Buchung festgeschrieben ist, zeigt
finalized(siehe Glossar). - Filtere und sortiere Buchungen nicht nach
amount; die API vergleicht dieses Attribut als Text (siehe Datentypen).