Webhooks
NovaFisko-Ereignisse auf Ihrem Server empfangen, einen Endpunkt erstellen und testen, die Zustellversuche verstehen und das Protokoll einsehen.
Ein Webhook ist ein HTTP-Aufruf, den NovaFisko an Ihren Server sendet, wenn ein Ereignis eintritt: eine importierte Rechnung, eine gebuchte Buchung, eine validierte MwSt.-Erklärung. Sie müssen die API nicht mehr abfragen, um zu erfahren, ob es Neues gibt.
Webhooks werden auf Ebene der Kanzlei konfiguriert und decken alle ihre Mandate ab. Nur die Administratoren und Manager der Kanzlei können sie verwalten.
Funktionsweise
- Sie registrieren eine URL und wählen die Ereignisse, die Sie empfangen möchten.
- NovaFisko übergibt Ihnen ein Secret für die Signatur, ein einziges Mal.
- Bei jedem Ereignis stellt NovaFisko eine Zustellung in die Warteschlange und sendet dann spätestens innerhalb einer Minute einen signierten
POST-Request an Ihre URL. - Ihr Server prüft die Signatur und antwortet in weniger als 10 Sekunden mit einem Status
2xx. - Bei einem Fehlschlag versucht NovaFisko es mit wachsendem Abstand erneut.
Einen Endpunkt erstellen
curl -X POST https://api.novafisko.com/v1/firms/Fd7hQ2mN8sXa/webhooks \
-H "Authorization: Bearer $NOVAFISKO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.atelier.example/hooks/novafisko",
"description": "ERP Atelier, production",
"events": ["document.imported", "document.booked", "entry.posted"],
"is_active": true
}'
| Feld | Pflicht | Beschreibung |
|---|---|---|
url |
ja | Empfangs-URL, max. 500 Zeichen. In der Produktion ist HTTPS Pflicht |
description |
nein | Freie Bezeichnung, max. 190 Zeichen |
events |
ja | Liste von Ereignissen oder ["*"] für alle |
is_active |
nein | Standardmäßig true |
Antwort 201:
{
"webhook": {
"id": 14,
"url": "https://erp.atelier.example/hooks/novafisko",
"description": "ERP Atelier, production",
"events": ["document.imported", "document.booked", "entry.posted"],
"is_active": true,
"secret_hint": "whsec_…x9Qd",
"last_delivery_at": null,
"last_status": null,
"created_at": "2026-10-05T09:20:00+00:00",
"updated_at": "2026-10-05T09:20:00+00:00"
},
"secret": "whsec_Zp3kV8nQ1bT6yR0mW4sH7cJ2dL9fA5gE1uXox9Qd"
}
Das secret wird nur bei der Erstellung zurückgegeben. Speichern Sie es sofort in Ihrem Secret-Manager. Spätere Lesezugriffe zeigen nur secret_hint, seine letzten vier Zeichen.
Anforderungen an die URL
- Das Schema muss
httpssein. - Die URL darf keine Zugangsdaten enthalten (
https://user:pass@...). - Der Host darf weder
localhostsein noch auf.localoder.internalenden noch auf eine private, eine Loopback- oder eine reservierte Adresse zeigen. - Weiterleitungen wird nicht gefolgt.
Eine abgelehnte URL antwortet mit 422 und dem Code webhook_url_refused. Eine Kanzlei kann höchstens 20 Endpunkte registrieren (webhook_limit_reached).
Endpunkte verwalten
| Aktion | Request | Antwort |
|---|---|---|
| Auflisten | GET /v1/firms/{firm}/webhooks |
{"data": [...]} |
| Ändern | PATCH /v1/firms/{firm}/webhooks/{id} |
{"webhook": {...}} |
| Löschen | DELETE /v1/firms/{firm}/webhooks/{id} |
204 |
| Testen | POST /v1/firms/{firm}/webhooks/{id}/test |
202 und die Zustellung |
| Protokoll | GET /v1/firms/{firm}/webhooks/{id}/deliveries |
Paginierte Liste |
| Katalog | GET /v1/webhooks/events |
Liste der Ereignisse und Beschreibungen |
Einen Endpunkt pausieren, ohne ihn zu löschen:
curl -X PATCH https://api.novafisko.com/v1/firms/Fd7hQ2mN8sXa/webhooks/14 \
-H "Authorization: Bearer $NOVAFISKO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
Ein Benutzer, der Mitglied der Kanzlei ist, aber nicht die erforderliche Rolle hat, erhält 403. Ein Benutzer außerhalb der Kanzlei erhält 404.
Der empfangene Request
POST /hooks/novafisko HTTP/1.1
Host: erp.atelier.example
Content-Type: application/json
User-Agent: NovaFisko-Webhooks/1.0
X-Novafisko-Event: entry.posted
X-Novafisko-Delivery: 90412
X-Novafisko-Signature: t=1791191400,v1=5f2b8c1e0a7d4f6b9c3e2a1d8f7b6c5a4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b
{"id":"evt_01jb6z3m8q2v7k4n5p9r0s1t2w","event":"entry.posted","api_version":"v1","created_at":"2026-10-05T09:23:20Z","firm":{"public_token":"Fd7hQ2mN8sXa","name":"Fiduciaire Exemple"},"company":{"public_token":"k3Jd9fPq2LmX","name":"Le Comptoir Montois SRL"},"data":{"id":1842,"reference":"ACH 2026/000212"}}
| Header | Inhalt |
|---|---|
X-Novafisko-Signature |
Zeitstempel und HMAC-SHA256-Signatur. Siehe Signaturen prüfen |
X-Novafisko-Event |
Name des Ereignisses |
X-Novafisko-Delivery |
Kennung der Zustellung, bei jedem Versuch identisch |
User-Agent |
NovaFisko-Webhooks/1.0 |
Umschlag
Alle Ereignisse teilen sich denselben Umschlag.
| Feld | Beschreibung |
|---|---|
id |
Eindeutige Kennung des Ereignisses mit dem Präfix evt_. Nutzen Sie sie, um Duplikate zu erkennen |
event |
Name des Ereignisses |
api_version |
v1 |
created_at |
Datum des Ereignisses, ISO 8601 in UTC |
firm |
public_token und Name der Kanzlei |
company |
public_token und Name des Mandats oder null bei einem Kanzleiereignis |
data |
Ereignisspezifischer Inhalt |
Ereignisse
| Ereignis | Auslöser |
|---|---|
document.imported |
Ein Beleg gelangt in das Mandat (Importer, Novadesko, Peppol) |
document.booked |
Ein Beleg wird gebucht |
entry.posted |
Eine Buchung wird gebucht |
entry.reversed |
Eine Buchung wird storniert |
bank.transaction_imported |
Eine Bankbewegung wird importiert |
vat.declaration_validated |
Eine MwSt.-Erklärung wird validiert |
fiscal_year.closed |
Ein Geschäftsjahr wird abgeschlossen |
peppol.status_changed |
Der Peppol-Status des Mandats ändert sich |
third_party.created |
Ein Geschäftspartner wird angelegt |
third_party.updated |
Ein Geschäftspartner wird geändert |
webhook.test |
Testversand, auf Anforderung |
Die folgenden Beispiele zeigen das Feld data jedes Ereignisses. Der Umschlag ist weggelassen.
document.imported und document.booked
{
"id": 7741,
"source": "novafisko",
"origin": "novafisko",
"type": "purchases",
"direction": "purchase",
"is_credit_note": false,
"number": "F-2026-0918",
"document_date": "2026-10-02",
"due_date": "2026-11-01",
"third_party_id": 41,
"third_party_name": "Brasserie des Collines SA",
"third_party_vat": "BE0999900134",
"currency": "EUR",
"amount_net": "1250.00",
"amount_vat": "262.50",
"amount_gross": "1512.50",
"status": "pending",
"journal_entry_id": null
}
Bei document.booked hat status den Wert booked, und journal_entry_id enthält die Kennung der Buchung.
entry.posted und entry.reversed
{
"id": 1842,
"reference": "ACH 2026/000212",
"journal_code": "ACH",
"fiscal_year_code": "2026",
"number": 212,
"entry_date": "2026-10-02",
"due_date": "2026-11-01",
"label": "Brasserie des Collines SA",
"document_reference": "F-2026-0918",
"status": "posted",
"origin": "manual",
"third_party_id": 41,
"total_debit": "1512.50",
"total_credit": "1512.50",
"lines_count": 3,
"reversed_entry_id": null,
"reversed_by_entry_id": null
}
Bei entry.reversed bezieht sich das Ereignis auf die ursprüngliche Buchung: status hat den Wert reversed, und reversed_by_entry_id bezeichnet die Stornobuchung. Die Stornobuchung selbst löst ihr eigenes entry.posted aus, mit ausgefülltem reversed_entry_id. Die Zeilen sind nicht enthalten: Lesen Sie sie mit GET entries/{id}.
bank.transaction_imported
{
"id": 30418,
"source": "novadesko",
"account_iban": "BE68539007547034",
"value_date": "2026-10-03",
"amount": "-1512.50",
"currency": "EUR",
"counterpart_name": "Brasserie des Collines SA",
"counterpart_iban": "BE71096123456769",
"communication": "+++091/8202/60018+++",
"status": "pending"
}
vat.declaration_validated
{
"id": 88,
"period_type": "quarterly",
"year": 2026,
"period": 3,
"label": "3T 2026",
"starts_on": "2026-07-01",
"ends_on": "2026-09-30",
"amount_due": "4218.36",
"amount_refund": "0.00",
"status": "validated",
"validated_at": "2026-10-05T09:31:02+00:00",
"journal_entry_id": 1860
}
fiscal_year.closed
{
"id": 6,
"code": "2025",
"starts_on": "2025-01-01",
"ends_on": "2025-12-31",
"closed_at": "2026-03-14T10:22:05+00:00"
}
peppol.status_changed
{
"id": 57,
"participant_id": "0208:0999900134",
"full_id": "iso6523-actorid-upis::0208:0999900134",
"status": "active",
"previous_status": "pending",
"incoming_enabled": true,
"activated_at": "2026-10-05T09:02:11+00:00"
}
previous_status hat bei der ersten Registrierung den Wert null.
third_party.created und third_party.updated
{
"id": 215,
"type": "supplier",
"code": "MARBOR",
"name": "Maraîchers du Borinage SC",
"vat_number": "BE0999900233",
"enterprise_number": "0999.900.233",
"country": "BE",
"email": "info@maraichers-borinage.example",
"peppol_identifier": "0208:0999900233",
"peppol_registered": true,
"changed": ["email", "phone"]
}
Das Feld changed, das nur bei third_party.updated vorhanden ist, listet die geänderten Felder auf. Technische Prüfungen (VIES, Peppol) lösen für sich allein kein Ereignis aus.
webhook.test
{
"message": "Test event sent from NovaFisko.",
"webhook_id": 14
}
Bei diesem Ereignis hat company den Wert null.
Versuche und Abstände
Ihr Server muss in weniger als 10 Sekunden mit einem Status 2xx antworten. Jede andere Antwort, eine Zeitüberschreitung oder ein Verbindungsfehler zählt als Fehlschlag.
| Versuch | Abstand nach dem vorherigen Fehlschlag |
|---|---|
| 1 | Sofort, innerhalb der Minute nach dem Ereignis |
| 2 | 1 Minute |
| 3 | 2 Minuten |
| 4 | 4 Minuten |
| 5 | 8 Minuten |
| 6 | 16 Minuten |
| 7 | 32 Minuten |
| 8 | 64 Minuten |
Nach dem achten Versuch, also nach etwas mehr als zwei Stunden, erhält die Zustellung den Status failed und wird nicht mehr wiederholt. Ein fehlschlagender Endpunkt wird nicht automatisch deaktiviert: Überwachen Sie last_status.
Die Zustellungen werden von einem geplanten Job ausgelöst, der jede Minute läuft. Ein Ereignis trifft daher mit einer Verzögerung von einigen Sekunden bis zu einer Minute ein. Die Zustellungen eines pausierten Endpunkts bleiben in der Warteschlange und werden versendet, sobald er wieder aktiviert wird.
Testen
curl -X POST https://api.novafisko.com/v1/firms/Fd7hQ2mN8sXa/webhooks/14/test \
-H "Authorization: Bearer $NOVAFISKO_TOKEN"
Das Ereignis webhook.test wird sofort gesendet, ohne erneuten Versuch bei einem Fehlschlag. Die Antwort ist immer 202 und enthält die Zustellung mit dem erreichten Status, delivered oder failed. Die Route ist auf 12 Tests pro Minute begrenzt.
{
"delivery": {
"id": 90455,
"event_id": "evt_01jb6zb1t4c8h2m6q0x3y7z9a5",
"event": "webhook.test",
"status": "delivered",
"attempts": 1,
"next_attempt_at": null,
"response_status": 200,
"response_excerpt": "ok",
"duration_ms": 184,
"delivered_at": "2026-10-05T09:40:12+00:00",
"created_at": "2026-10-05T09:40:12+00:00",
"payload": {"id": "evt_01jb6zb1t4c8h2m6q0x3y7z9a5", "event": "webhook.test", "api_version": "v1", "created_at": "2026-10-05T09:40:12Z", "firm": {"public_token": "Fd7hQ2mN8sXa", "name": "Fiduciaire Exemple"}, "company": null, "data": {"message": "Test event sent from NovaFisko.", "webhook_id": 14}}
}
}
Zustellprotokoll
curl "https://api.novafisko.com/v1/firms/Fd7hQ2mN8sXa/webhooks/14/deliveries?status=failed&per_page=25" \
-H "Authorization: Bearer $NOVAFISKO_TOKEN"
| Parameter | Beschreibung |
|---|---|
status |
pending, delivered oder failed |
event |
Name eines Ereignisses |
per_page |
Standardmäßig 25, höchstens 100 |
page |
Seitennummer |
{
"data": [
{
"id": 90412,
"event_id": "evt_01jb6z3m8q2v7k4n5p9r0s1t2w",
"event": "entry.posted",
"status": "failed",
"attempts": 8,
"next_attempt_at": null,
"response_status": 500,
"response_excerpt": "Internal Server Error",
"duration_ms": 412,
"delivered_at": null,
"created_at": "2026-10-05T09:23:20+00:00",
"payload": {"id": "evt_01jb6z3m8q2v7k4n5p9r0s1t2w", "event": "entry.posted"}
}
],
"meta": {"current_page": 1, "last_page": 1, "per_page": 25, "total": 1}
}
response_excerpt enthält die ersten 2000 Zeichen der Antwort Ihres Servers oder die Fehlermeldung der Verbindung. payload ist der exakte Body, der gesendet wurde: Sie können ihn selbst erneut einspielen, nachdem Sie eine Störung behoben haben. Abgeschlossene Zustellungen (delivered oder failed) werden 30 Tage aufbewahrt.
Bewährte Vorgehensweisen
- Prüfen Sie immer die Signatur, bevor Sie irgendetwas verarbeiten. Siehe Signaturen prüfen.
- Antworten Sie schnell. Bestätigen Sie den Empfang mit einem
200, legen Sie das Ereignis in eine Warteschlange und verarbeiten Sie es danach. Eine lange Verarbeitung führt zu Zeitüberschreitungen und Duplikaten. - Entfernen Sie Duplikate anhand der
id. Dieselbe Zustellung kann Sie mehrmals erreichen, wenn Ihre Antwort verloren gegangen ist. Bewahren Sie dieidbereits verarbeiteter Ereignisse auf. - Setzen Sie keine Reihenfolge voraus. Zwei zeitlich nahe Ereignisse können in falscher Reihenfolge eintreffen, vor allem nach einem erneuten Versuch. Stützen Sie sich auf
created_atund lesen Sie die Ressource im Zweifel erneut. - Behandeln Sie das Ereignis als Signal. Der Inhalt von
dataist eine Zusammenfassung. Für den vollständigen und aktuellen Zustand rufen Sie die API auf. - Ignorieren Sie unbekannte Ereignisse. Neue Ereignisse und neue Felder können ohne Versionswechsel hinzukommen. Antworten Sie mit
200auf das, was Sie nicht kennen. - Rechnen Sie mit Lastspitzen. Ein Massenimport (CODA-Auszug, Novadesko-Synchronisierung) erzeugt eine Zustellung pro Beleg oder pro Bewegung.
- Kombinieren Sie mit
sync/pull. Nutzen Sie den Webhook als Auslöser und den Änderungsstream als maßgebliche Quelle. Siehe Fortlaufend synchronisieren.
Die Demo-Kanzlei sendet keine Webhooks, und die Verwaltung der Endpunkte ist dort blockiert (403, Code demo_mode).