Webhooks
Invostaq sends webhook events when invoice status changes on the Peppol network. Set up a webhook endpoint to receive delivery confirmations, failure notifications, and inbound invoices in real time.
Prerequisites
- An active Invostaq account
- A publicly accessible HTTPS endpoint that can receive POST requests
Setup
Register your webhook URL from the Invostaq dashboard under Settings > Webhooks. Provide:
- URL — your HTTPS endpoint (e.g.,
https://your-app.com/webhooks/invostaq) - Secret — a shared secret for validating webhook authenticity
- Name — a label to identify this webhook in the dashboard
Your endpoint must respond with 200 OK to every webhook delivery. The response body is ignored.
Events
transaction.sent — Invoice delivered
The invoice was successfully delivered to the recipient's Access Point.
{
"transactionId": "txn_abc123def456",
"eventType": "transaction.sent",
"status": "Delivered"
}
Effect: The invoice status updates from Processing to Delivered.
transaction.failed — Delivery failed
The Peppol network rejected or could not deliver the invoice.
{
"transactionId": "txn_abc123def456",
"eventType": "transaction.failed",
"status": "Failed",
"reason": "Recipient Access Point unreachable"
}
Effect: The invoice status updates to Failed with the failure reason.
transaction.received — Inbound invoice
A trading partner sent an invoice to your Peppol participant ID.
{
"eventType": "transaction.received",
"payload": {
"transactionId": "inbound-tx-67890",
"receiverId": "0196:971501234567",
"docInstanceId": "doc-instance-id"
},
"senderParticipantId": "0088:9876543210987",
"timestamp": "2024-06-15T10:30:00Z"
}
Effect: Invostaq fetches the UBL XML, archives the original document, parses the invoice metadata, and creates a new invoice record with status Received.
webhooks.test — Connectivity test
Sent when you first configure a webhook URL. No action required — just respond with 200 OK.
{
"eventType": "webhooks.test",
"timestamp": "2024-06-15T09:01:00Z"
}
Security
Secret validation
Webhook deliveries include a secret header that you should validate before processing:
X-Webhook-Secret: your_configured_secret
Use a timing-safe comparison to prevent timing attacks:
import { timingSafeEqual } from "crypto";
function verifyWebhookSecret(received: string, expected: string): boolean {
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
Retry behavior
| Scenario | Response | Result |
|---|---|---|
| Valid event, processed successfully | 200 OK | Event recorded |
| Invalid secret or malformed JSON | 200 OK | Silently dropped (prevents retry loop) |
| Internal processing failure | 500 | Peppol Access Point retries delivery |
Why 200 OK for invalid events? The Peppol Access Point retries on non-2xx responses. Returning 200 for permanently bad events prevents infinite retries that would never succeed.
Invoice status flow
Outbound:
Draft → Processing → Delivered
→ Failed
Inbound:
Received → Approved → Synced
→ Rejected
| Status | Meaning |
|---|---|
Processing | Submitted to Peppol, awaiting Access Point response |
Delivered | Successfully delivered to recipient (via transaction.sent) |
Failed | Delivery failed (via transaction.failed) |
Received | Inbound invoice received from trading partner |
Best practices
- Always return
200 OK— don't return errors from your webhook handler, even if your internal processing fails. Queue the event for async processing instead. - Make your handler idempotent — you may receive the same event more than once. Use
transactionIdto deduplicate. - Process quickly — return
200 OKwithin a few seconds. If you need to do heavy processing, enqueue the event and handle it asynchronously. - Validate the secret — always check the
X-Webhook-Secretheader before trusting the payload.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Returning non-200 for invalid events | Infinite retry loop from Access Point | Always return 200 OK, even for bad events |
| Not deduplicating events | Same invoice processed twice | Check transactionId before processing |
| Synchronous heavy processing | Webhook timeout, missed events | Enqueue and process asynchronously |
Comparing secrets with === | Vulnerable to timing attacks | Use timingSafeEqual |