Zum Inhalt springen
Dokumentation
Deutsch
App öffnen

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

  1. Sie registrieren eine URL und wählen die Ereignisse, die Sie empfangen möchten.
  2. NovaFisko übergibt Ihnen ein Secret für die Signatur, ein einziges Mal.
  3. 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.
  4. Ihr Server prüft die Signatur und antwortet in weniger als 10 Sekunden mit einem Status 2xx.
  5. 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"
}
Achtung

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 https sein.
  • Die URL darf keine Zugangsdaten enthalten (https://user:pass@...).
  • Der Host darf weder localhost sein noch auf .local oder .internal enden 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.

Hinweis

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 die id bereits 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_at und lesen Sie die Ressource im Zweifel erneut.
  • Behandeln Sie das Ereignis als Signal. Der Inhalt von data ist 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 200 auf 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.
Achtung

Die Demo-Kanzlei sendet keine Webhooks, und die Verwaltung der Endpunkte ist dort blockiert (403, Code demo_mode).

Siehe auch