Entwickler & API
epago ist doppelte Buchführung mit DATEV-Kontenrahmen und GoBD-Regeln als Datenbank-Trigger. Die Public API v1 ist kein zweiter, laxerer Weg daneben: jeder Schreibzugriff läuft durch dieselbe Prüfschicht wie die Oberfläche — Soll gleich Haben, Steuerautomatik, Periodensperre, Storno statt Löschen. Zugang bekommst du über einen API-Schlüssel mit Scope read oderwrite, in zwei Umgebungen: einer Sandbox zum Ausprobieren und dem Live-Zugang zu deiner echten Buchhaltung.
Zuerst: die Sandbox
Bevor du gegen deine echte Buchhaltung testest, nimm die Sandbox. Jeder Account bekommt einen eigenen Test-Mandanten mit einem kleinen Startbestand — Kunden, einen Lieferanten, Rechnungen und Buchungen — unabhängig von deinen echten Daten. Ein Sandbox-Schlüssel beginnt mit epago_test_ und erreicht ausschließlich diesen Test-Mandanten, nie deine echte Buchhaltung.
- Anlegen und Zurücksetzen im Tab API-Schlüssel unter Mein Konto in der App.
- Zurücksetzen erzeugt einen frischen Test-Mandanten mit neuem Startbestand. Der alte bleibt als abgeschlossener Stand stehen — GoBD gilt auch in der Sandbox, gelöscht wird nichts.
- Aus der Sandbox geht nichts nach draußen: keine E-Mail, keine ELSTER-Übermittlung, keine Bankverbindung, keine Belastung eines Zahlungsmittels.
- Ein eigener Login in die Sandbox existiert nicht — erreichbar ist sie ausschließlich über die API, mit dem Sandbox-Schlüssel.
- Rate-Limit 30 Anfragen pro Minute (Live-Schlüssel: 60 — beides am Schlüssel einstellbar).
Live-Schlüssel für deine echte Buchhaltung legst du an derselben Stelle an, sobald du mit der Sandbox durch bist.
Quickstart in drei Schritten
1. Sandbox-Schlüssel anlegen
Unter Mein Konto → API-Schlüssel einen Schlüssel mit Umgebung Sandbox und Scope write erzeugen. Er beginnt mit epago_test_ und wird nur einmal angezeigt — sofort sichern.
2. Verbindung testen
Die Antwort nennt Mandant, Schlüssel-Scopes und die Umgebung — an einem Sandbox-Schlüssel steht dort "apiKey.umgebung": "sandbox".
curl -H "Authorization: Bearer $EPAGO_KEY" \
https://app.epago.de/api/v1/me{
"tenant": { "userId": "sandbox:…", "companyName": "Testmandant (Sandbox)" },
"apiKey": { "keyId": "…", "permissions": ["read", "write"], "umgebung": "sandbox" }
}3. Erste Buchung als Entwurf
Die vereinfachte Form: Bruttobetrag, Geldkonto, Sachkonto — der Server baut die Buchungszeilen samt Steuerzeile mit dem Kontenrahmen des Mandanten. Ein Entwurf (status: "draft", auch der Default) bucht nichts fest und lässt sich wieder löschen — der richtige Einstieg für die erste Anfrage. Die Kontonummern sind SKR03 (Quelle: wissen/datev-kontenrahmen/skr03-2026.json im Repository) — 1000 Kasse, 8400 Erlöse 19 % USt; im SKR04 sind es andere Nummern, die Antwort nennt den Kontenrahmen des Mandanten mit.
curl -X POST -H "Authorization: Bearer $EPAGO_KEY" \
-H "Content-Type: application/json" \
https://app.epago.de/api/v1/journal-entries \
-d '{
"date": "2026-06-12",
"description": "Barverkauf",
"status": "draft",
"betrag": 119.00,
"bruttoKonto": "1000",
"sachKonto": "8400"
}'Weitere Beispiele — Buchung mit fertigen Zeilen, Storno, Zahlung an einem Beleg — stehen in der Referenz unten und in der Postman-Sammlung.
API-Referenz
Alle siebzehn Endpunkte mit Parametern, Beispielen und Antwortschemata — geladen zur Laufzeit aus derselben Spezifikation, aus der auch die Postman-Sammlung entsteht.
Authentifizierung und Grenzen
Schlüssel senden
Als Authorization: Bearer epago_… oder im x-api-key-Header. Ein Schlüssel gehört zu genau einem Mandanten; der Mandant kommt immer aus dem Schlüssel, nie aus dem Anfragekörper.
Scopes
read liest alles. write legt Buchungen und Ausgangsrechnungen an, storniert, erfasst Zahlungen und lädt Belege hoch — schließt read ein. Nie Default: beim Erzeugen eines Schlüssels explizit wählen.
Rate-Limit
60 Anfragen/Minute je Live-Schlüssel, 30 je Sandbox-Schlüssel (Schiebefenster, einstellbar am Schlüssel), zusätzlich 600/Minute je IP. Das Limit gilt über alle Instanzen hinweg. Jede Antwort trägt den Zustand des Fensters in X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset; eine 429 zusätzlich in Retry-After (Sekunden).
Widerruf
Ein Schlüssel lässt sich jederzeit unter Mein Konto widerrufen oder ablaufen lassen — die Sperre wirkt sofort, ohne Cache-Verzögerung.
Fehlerformat und -codes
Jeder Fehler hat denselben Aufbau, ohne Stacktrace, ohne interne Pfade:
{ "error": { "code": "insufficient_scope", "message": "…" } }| HTTP | Code | Bedeutung |
|---|---|---|
| 400 | bad_request / validation_error | Pflichtfeld fehlt, Soll ist nicht gleich Haben, ungueltige Steuerzeile oder Status. |
| 400 | file_too_large / unsupported_media_type | Beleg-Upload: Datei ueber 10 MB oder nicht erlaubter Dateityp. Achtung: die Plattform begrenzt den Request schon bei 4,5 MB und antwortet dann mit einem nackten 413 ohne dieses Fehlerformat — als Base64 passt eine Datei bis rund 3 MB durch. |
| 400 | content_mismatch | Beleg-Upload: Dateiinhalt passt nicht zum angegebenen mimeType (Magic-Bytes-Pruefung). |
| 400 | zahlungsart_erforderlich / begruendung_erforderlich / rueckzahlungsgrund_erforderlich | Eine Zahlung laesst einen Rest offen oder ist eine Rueckzahlung: die Antwort traegt Vorschlag, Restbetrag und Optionen mit. |
| 401 | missing_api_key / invalid_api_key / api_key_expired / api_key_revoked | Schluessel fehlt, ist ungueltig, abgelaufen oder widerrufen. |
| 402 | feature_locked / account_gesperrt | Die API gehoert nicht zum Tarif des Mandanten, oder der Zugang ist wegen einer offenen Zahlung gesperrt. |
| 403 | insufficient_scope | Der Schluessel hat den benoetigten Scope nicht (zum Beispiel read auf einer Schreib-Route). |
| 404 | not_found | Ressource nicht gefunden — auch bei einer ID aus einem fremden Mandanten. |
| 409 | period_closed / conflict / cannot_reverse | Festgeschriebene Periode, oder eine Buchung, die nicht stornierbar ist. |
| 409 | payment_not_allowed | Auf diesen Beleg geht keine Zahlung ein (Entwurf, storniert, unbekannter Status). |
| 423 | konto_gesperrt_loeschantrag | Der Mandant hat die Loeschung seiner Daten beantragt; bis zur Ruecknahme wird nicht mehr verarbeitet. |
| 429 | rate_limit_exceeded | Rate-Limit ueberschritten. Retry-After nennt die Wartezeit in Sekunden. |
| 500 | internal_error / reverse_incomplete | Serverfehler. Bei reverse_incomplete: die Gegenbuchung wurde angelegt, die Markierung des Originals ist fehlgeschlagen — nicht automatisch wiederholen, Support kontaktieren. |
Versionierung und Änderungsverlauf
Die Version steht im Pfad (/api/v1). Innerhalb von v1 wird nur additiv geändert: neue Endpunkte, neue optionale Felder, neue Fehlerfelder. Ein bestehendes Feld verschwindet nicht und wechselt nicht die Bedeutung. info.version in der Spezifikation folgt SemVer; eine nicht additive Änderung bekäme /api/v2.
- Idempotenz fuer POST /invoices: mit dem Header Idempotency-Key legt eine Wiederholung keine zweite Rechnung an. Die Antwort ist dann 201 mit der bestehenden Rechnung und wiederholung true; eine Wiederholung bucht nie ein zweites Mal.
- Derselbe Idempotency-Key mit anderem Inhalt wird mit 409 idempotency_conflict abgewiesen. Ohne Header bleibt alles wie bisher.
- POST /invoices und Zahlungen verlangen neben api_v1 das Tarif-Merkmal faktura, Buchungen und Storno das Merkmal buchen, der Beleg-Upload das Merkmal belege, wie die Oberflaeche. Solange die Tarife nicht scharf geschaltet sind, blockiert das nichts. Lesen bleibt mit api_v1 offen.
- Kunden lesen: GET /customers liefert den Kundenstamm seitenweise (limit bis 200, offset) mit Suche nach Name oder Kundennummer, damit ein Client die customerId einer Rechnung findet.
- Ausgangsrechnung anlegen: POST /invoices legt eine Rechnung ueber dieselbe Schreibschicht wie die Oberflaeche an. Leistungszeit und Faelligkeit (dueDate oder paymentTermDays) sind Pflicht, mit buchen true wird die Rechnung sofort gebucht.
- Scheitert die Buchung, bleibt der Entwurf stehen und buchung.fehler nennt Grund und Handlungsoption, zum Beispiel ein fehlendes Konto aus dem Kontenrahmen.
- Offene Posten: GET /invoices/open liefert die offenen Ausgangsrechnungen mit offenem Betrag, Faelligkeitszustand, Tagen ueberfaellig und Mahnstand, sortiert nach Faelligkeit und mit dem Filter nurUeberfaellig.
- Der Filter documentType=outgoing bzw. incoming an GET /invoices wirkt jetzt. Bisher traf er keine Zeile, weil die Spalte die Werte outgoing_invoice und incoming_invoice fuehrt.
- Das Schema Invoice beschreibt die tatsaechlich gelieferten Felder (documentNumber, grossAmount, netAmount, taxAmount, dueDate, journalEntryId). Die nie gelieferten Namen invoiceNumber und totalGross sind als veraltet gekennzeichnet.
- Jede Antwort traegt den Zustand des Rate-Limit-Fensters: X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset (Unix-Zeit in Sekunden).
- Die Absage mit Status 429 nennt zusaetzlich Retry-After in Sekunden (RFC 9110, 10.2.3).
- Das Limit gilt ueber alle Instanzen hinweg. Vorher zaehlte jede Function-Instanz fuer sich, das zugesagte Limit war damit nicht belastbar.
- GET /me nennt die Umgebung des Schlüssels (umgebung: live oder sandbox). Sandbox-Schlüssel (epago_test_…) arbeiten auf dem Testmandanten des Accounts, den man unter Mein Konto → API-Schlüssel anlegt und zurücksetzt; sie erreichen die echte Buchhaltung nie und haben ein Limit von 30 Anfragen pro Minute.
- Zahlungen an Belegen: POST /invoices/{id}/payments erfasst eine Zahlung, GET /invoices/{id}/payments liest die erfassten Zahlungen. Bleibt nach der Zahlung ein Rest offen, ist zahlungsart Pflicht (vollzahlung, teilzahlung, skonto, forderungsausfall); die Absage traegt Vorschlag, Restbetrag und Optionen mit, statt eine Entgeltminderung zu raten.
- Vereinfachte Buchungsform: POST /journal-entries nimmt neben lines[] auch betrag, bruttoKonto und sachKonto. Die Buchungszeilen samt Steuerzeile baut der Server mit dem Kontenrahmen des Mandanten, und die Antwort sagt mit zeilen, steuerschluessel, split und kontenrahmen, was daraus geworden ist. Mit seite gibt der Aufrufer die Soll- oder Habenseite an, wo der Steuerfall sie nicht vorgibt.
- Jede Operation nennt ihren Scope in x-scope (read oder write).
- Die Absagen des Zugangs stehen jetzt an jeder Operation: 402 bei fehlendem Tarif oder gesperrtem Zugang (feature_locked, account_gesperrt), 423 bei einem laufenden Loeschantrag (konto_gesperrt_loeschantrag). Der 402 traegt seinen Fehlercode, statt als bad_request herauszukommen.
- Die Beschreibung der Spezifikation nennt Grenzen, Fehlerformat und Versionsregel.
- Start der Public API v1 mit 14 Endpunkten: Verbindungstest (/me), Kontenplan und Kontenblatt, Buchungen lesen, anlegen und stornieren, Rechnungen lesen, Beleg hochladen, BWA, Umsatzsteuer-Voranmeldung und Saldenliste.
- Authentifizierung per API-Schluessel (Authorization: Bearer oder x-api-key) mit den Scopes read und write.
- Schreibzugriffe laufen durch dieselbe Pruefschicht wie die Oberflaeche: Soll gleich Haben, Steuerautomatik, Periodensperre, Storno statt Loeschen.
- Kein DELETE: gebuchte Saetze werden nicht geloescht, sondern storniert.
Postman
Die Sammlung entsteht aus derselben Spezifikation wie die Referenz oben — kein von Hand gepflegtes Duplikat.
- Beide Dateien in Postman importieren (Sammlung und Umgebung).
- Die Umgebung epago API oben rechts auswählen und in der Variable
apiKeydeinen Schlüssel eintragen —baseUrlsteht bereits aufhttps://app.epago.de/api/v1. - Anfrage auswählen und senden — für einen ersten Test eignet sich
GET /memit einem Sandbox-Schlüssel.
MCP-Server
epago-mcp ist ein zweiter Zugang zu derselben API — für Claude, Codex und andere MCP-fähige Werkzeuge. Er läuft lokal (stdio), braucht denselben API-Schlüssel wie ein direkter Aufruf und schreibt über dieselbe Prüfschicht. Installation, Konfiguration und alle Werkzeuge stehen auf einer eigenen Seite.
Was die API nicht kann
Damit hier nichts steht, was die API nicht hält. Seit Version 1.3.0 lassen sich Kunden lesen (GET /customers), Ausgangsrechnungen anlegen und auf Wunsch sofort buchen (POST /invoices) und offene Posten mit Fälligkeit abrufen (GET /invoices/open). Aktuell nicht möglich sind eine Mahnung auslösen, Kunden anlegen oder ändern, ein PDF eines Belegs abrufen und jedes Löschen — gebuchte Sätze werden storniert, nicht gelöscht (GoBD). Neue Möglichkeiten stehen im Änderungsverlauf oben, sobald sie da sind.