Webhooks
Receive NovaFisko events on your server, create and test an endpoint, understand delivery attempts and consult the log.
A webhook is an HTTP call that NovaFisko sends to your server when an event occurs: an invoice imported, an entry posted, a VAT return validated. You no longer have to poll the API to find out whether there is anything new.
Webhooks are configured at firm level and cover all its companies. Only the administrators and managers of the firm can manage them.
How it works
- You register a URL and choose the events to receive.
- NovaFisko gives you a signing secret, once only.
- At each event, NovaFisko queues a delivery, then sends a signed
POSTrequest to your URL, within the minute at the latest. - Your server verifies the signature and responds with a
2xxstatus in less than 10 seconds. - On failure, NovaFisko retries with an increasing delay.
Create an endpoint
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
}'
| Field | Required | Description |
|---|---|---|
url |
yes | Receiving URL, 500 characters max. HTTPS mandatory in production |
description |
no | Free label, 190 characters max |
events |
yes | List of events, or ["*"] for all |
is_active |
no | true by default |
Response 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"
}
The secret is only returned at creation. Save it immediately in your secrets manager. Later reads only show secret_hint, its last four characters.
URL constraints
- The scheme must be
https. - The URL must not contain credentials (
https://user:pass@...). - The host must not be
localhost, nor end with.localor.internal, nor point to a private, loopback or reserved address. - Redirects are not followed.
A refused URL responds 422 with the webhook_url_refused code. A firm can register at most 20 endpoints (webhook_limit_reached).
Manage endpoints
| Action | Request | Response |
|---|---|---|
| List | GET /v1/firms/{firm}/webhooks |
{"data": [...]} |
| Update | PATCH /v1/firms/{firm}/webhooks/{id} |
{"webhook": {...}} |
| Delete | DELETE /v1/firms/{firm}/webhooks/{id} |
204 |
| Test | POST /v1/firms/{firm}/webhooks/{id}/test |
202 and the delivery |
| Log | GET /v1/firms/{firm}/webhooks/{id}/deliveries |
Paginated list |
| Catalogue | GET /v1/webhooks/events |
List of events and descriptions |
Pausing an endpoint without deleting it:
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}'
A user who is a member of the firm without the required role receives 403. A user outside the firm receives 404.
The request received
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 | Content |
|---|---|
X-Novafisko-Signature |
Timestamp and HMAC-SHA256 signature. See Verify signatures |
X-Novafisko-Event |
Name of the event |
X-Novafisko-Delivery |
Identifier of the delivery, identical from one attempt to the next |
User-Agent |
NovaFisko-Webhooks/1.0 |
Envelope
All events share the same envelope.
| Field | Description |
|---|---|
id |
Unique identifier of the event, prefixed with evt_. Use it to deduplicate |
event |
Name of the event |
api_version |
v1 |
created_at |
Date of the event, ISO 8601 in UTC |
firm |
public_token and name of the firm |
company |
public_token and name of the company, or null for a firm event |
data |
Content specific to the event |
Events
| Event | Trigger |
|---|---|
document.imported |
A document enters the company (importer, Novadesko, Peppol) |
document.booked |
A document is booked |
entry.posted |
An entry is posted |
entry.reversed |
An entry is reversed |
bank.transaction_imported |
A bank transaction is imported |
vat.declaration_validated |
A VAT return is validated |
fiscal_year.closed |
A fiscal year is closed |
peppol.status_changed |
The Peppol status of the company changes |
third_party.created |
A third party is created |
third_party.updated |
A third party is modified |
webhook.test |
Test send, on demand |
The examples below show the data field of each event. The envelope is omitted.
document.imported and 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
}
For document.booked, status is booked and journal_entry_id carries the identifier of the entry.
entry.posted and 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
}
For entry.reversed, the event concerns the original entry: status is reversed and reversed_by_entry_id points to the reversal. The reversal itself triggers its own entry.posted, with reversed_entry_id filled in. Lines are not included: read them with 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 is null at the first registration.
third_party.created and 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"]
}
The changed field, present only for third_party.updated, lists the modified fields. Technical checks (VIES, Peppol) do not trigger an event on their own.
webhook.test
{
"message": "Test event sent from NovaFisko.",
"webhook_id": 14
}
For this event, company is null.
Attempts and delays
Your server must respond with a 2xx status in less than 10 seconds. Any other response, a timeout or a connection error counts as a failure.
| Attempt | Delay after the previous failure |
|---|---|
| 1 | Immediate, within the minute following the event |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 4 minutes |
| 5 | 8 minutes |
| 6 | 16 minutes |
| 7 | 32 minutes |
| 8 | 64 minutes |
After the eighth attempt, that is a little over two hours, the delivery moves to the failed status and is no longer retried. A failing endpoint is not disabled automatically: monitor last_status.
Deliveries are sent by a scheduled job every minute. An event therefore arrives with a delay of a few seconds to one minute. The deliveries of a paused endpoint remain pending and are sent as soon as it is reactivated.
Testing
curl -X POST https://api.novafisko.com/v1/firms/Fd7hQ2mN8sXa/webhooks/14/test \
-H "Authorization: Bearer $NOVAFISKO_TOKEN"
The webhook.test event is sent immediately, with no retry on failure. The response is always 202 and contains the delivery with the resulting status, delivered or failed. The route is limited to 12 tests per minute.
{
"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}}
}
}
Delivery log
curl "https://api.novafisko.com/v1/firms/Fd7hQ2mN8sXa/webhooks/14/deliveries?status=failed&per_page=25" \
-H "Authorization: Bearer $NOVAFISKO_TOKEN"
| Parameter | Description |
|---|---|
status |
pending, delivered or failed |
event |
Name of an event |
per_page |
25 by default, 100 at most |
page |
Page number |
{
"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 contains the first 2000 characters of your server's response, or the connection error message. payload is the exact body that was sent: you can replay it yourself after fixing an outage. Finished deliveries (delivered or failed) are kept for 30 days.
Best practices
- Always verify the signature before processing anything. See Verify signatures.
- Respond quickly. Acknowledge receipt with a
200, put the event in a queue and process it afterwards. Long processing causes timeouts and duplicates. - Deduplicate on
id. The same delivery may reach you several times if your response was lost. Keep theidvalues of events already processed. - Assume no order. Two events close together may arrive out of order, especially after a retry. Rely on
created_atand read the resource again when in doubt. - Treat the event as a signal. The content of
datais a summary. For the complete and current state, call the API. - Ignore unknown events. New events and new fields may appear without a version change. Respond
200to anything you do not know. - Plan for bursts. A bulk import (CODA statement, Novadesko synchronisation) produces one delivery per document or per transaction.
- Combine with
sync/pull. Use the webhook as a trigger and the change stream as the source of truth. See Continuous sync.
The demo firm emits no webhooks and endpoint management is blocked there (403, code demo_mode).