/docs/webhooks
Webhooks documentation
Complete technical reference for the 16 Invocore events, HMAC signatures, retry behavior, and integration samples.
Overview
Invocore sends HTTP POST requests to your endpoint URL whenever a supported event happens inside your organization. Every request is signed with HMAC-SHA256 and retried up to three times with exponential backoff (5s → 25s → 125s) on failure.
- 16 native events — from invoice creation to export failure
- HMAC-SHA256 signature per request, designed for timing-safe comparison
- Retry with exponential backoff: 5s, 25s, 125s
- Delivery log in admin + one-click retry for failed deliveries
- Conditional filters (field / operator / value) against event payload
Supported events
Every event uses the same envelope shape: event, timestamp, organization_id, data. Sample payloads below are realistic and match the exact format your endpoint will receive.
invoice.createdFired when a new invoice is created.
Sample payload
{
"event": "invoice.created",
"timestamp": "2026-02-25T12:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "inv_01HABC",
"number": "RE-2026-0042",
"status": "draft",
"currency": "EUR",
"issue_date": "2026-02-25",
"due_date": "2026-03-25",
"total_with_vat": "1190.00",
"buyer": {
"name": "Mustermann GmbH",
"email": "invoice@mustermann.de"
}
}
}invoice.sentFired when an invoice is sent via email or Peppol.
Sample payload
{
"event": "invoice.sent",
"timestamp": "2026-02-25T12:05:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "inv_01HABC",
"number": "RE-2026-0042",
"status": "sent",
"channel": "email",
"sent_to": "invoice@mustermann.de"
}
}invoice.paidFired when an invoice is marked as fully paid.
Sample payload
{
"event": "invoice.paid",
"timestamp": "2026-03-01T09:30:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "inv_01HABC",
"number": "RE-2026-0042",
"status": "paid",
"paid_at": "2026-03-01T09:30:00Z",
"paid_amount": "1190.00",
"payment_method": "bank_transfer"
}
}invoice.overdueFired when an invoice passes its due date.
Sample payload
{
"event": "invoice.overdue",
"timestamp": "2026-03-26T00:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "inv_01HABC",
"number": "RE-2026-0042",
"status": "overdue",
"overdue_days": 1,
"due_date": "2026-03-25"
}
}invoice.updatedFired when invoice fields are modified.
Sample payload
{
"event": "invoice.updated",
"timestamp": "2026-02-25T14:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "inv_01HABC",
"number": "RE-2026-0042",
"total_with_vat": "1309.00"
}
}invoice.deletedFired when an invoice is deleted.
Sample payload
{
"event": "invoice.deleted",
"timestamp": "2026-02-25T15:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "inv_01HABC",
"number": "RE-2026-0042"
}
}contact.createdFired when a new contact is created.
Sample payload
{
"event": "contact.created",
"timestamp": "2026-02-25T10:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "con_01HDEF",
"firma_name": "Mustermann GmbH",
"email": "info@mustermann.de",
"vat_id": "DE123456789"
}
}contact.updatedFired when a contact is updated.
Sample payload
{
"event": "contact.updated",
"timestamp": "2026-02-25T11:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "con_01HDEF",
"firma_name": "Mustermann GmbH",
"email": "billing@mustermann.de"
}
}contact.deletedFired when a contact is deleted.
Sample payload
{
"event": "contact.deleted",
"timestamp": "2026-02-25T16:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "con_01HDEF",
"firma_name": "Mustermann GmbH"
}
}payment.receivedFired when a payment is recorded (Stripe, PayPal, or manual).
Sample payload
{
"event": "payment.received",
"timestamp": "2026-03-01T09:30:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "pay_01HGHI",
"invoice_id": "inv_01HABC",
"amount": "1190.00",
"currency": "EUR",
"provider": "stripe",
"paid_at": "2026-03-01T09:30:00Z"
}
}payment.overdueFired when a payment is overdue and a reminder is due.
Sample payload
{
"event": "payment.overdue",
"timestamp": "2026-03-26T00:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"invoice_id": "inv_01HABC",
"invoice_number": "RE-2026-0042",
"overdue_amount": "1190.00",
"currency": "EUR",
"overdue_days": 1
}
}document.uploadedFired when a document is uploaded.
Sample payload
{
"event": "document.uploaded",
"timestamp": "2026-02-25T13:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "doc_01HJKL",
"filename": "rechnung_januar.pdf",
"file_size_bytes": 245760,
"mime_type": "application/pdf",
"uploaded_by": "user@example.com"
}
}export.startedFired when an export job starts (DATEV, CSV, Excel).
Sample payload
{
"event": "export.started",
"timestamp": "2026-02-25T20:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "exp_01HMNO",
"format": "datev",
"started_at": "2026-02-25T20:00:00Z"
}
}export.failedFired when an export job fails.
Sample payload
{
"event": "export.failed",
"timestamp": "2026-02-25T20:01:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "exp_01HMNO",
"format": "datev",
"error": "No documents found in selected period."
}
}approval.requestedFired when an invoice is submitted for approval.
Sample payload
{
"event": "approval.requested",
"timestamp": "2026-02-25T12:30:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "apr_01HPQR",
"invoice_id": "inv_01HABC",
"submitted_by": "user@example.com",
"submitted_at": "2026-02-25T12:30:00Z"
}
}approval.decidedFired when an approval decision is made (approved / rejected).
Sample payload
{
"event": "approval.decided",
"timestamp": "2026-02-25T13:00:00Z",
"organization_id": "org_01HXYZ",
"data": {
"id": "apr_01HPQR",
"invoice_id": "inv_01HABC",
"decision": "approved",
"decided_by": "manager@example.com",
"decided_at": "2026-02-25T13:00:00Z"
}
}HTTP headers
Every request includes the following headers:
| Header | Description |
|---|---|
X-Invocore-Event | Event type (e.g. invoice.paid) |
X-Invocore-Signature | HMAC-SHA256 in the form sha256=<hex> |
X-Invocore-Delivery | Unique delivery ID (use for idempotency and dedup) |
X-Invocore-Timestamp | ISO-8601 timestamp of the first delivery attempt |
User-Agent | Invocore-Webhook/1.0 |
Content-Type | application/json; charset=utf-8 |
Signature verification
Verify every incoming request before handling the payload. Use a timing-safe comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node, hash_equals in PHP).
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your-webhook-secret"
@app.route("/webhooks/invocore", methods=["POST"])
def invocore_webhook():
signature_header = request.headers.get("X-Invocore-Signature", "")
if not signature_header.startswith("sha256="):
abort(401)
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.get_data(),
hashlib.sha256,
).hexdigest()
received = signature_header.split("=", 1)[1]
if not hmac.compare_digest(expected, received):
abort(401)
event = request.headers.get("X-Invocore-Event")
payload = request.json
# ... handle payload["data"] ...
return "", 200
import express from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = process.env.INVOCORE_WEBHOOK_SECRET;
// Use raw body so the signature can be computed byte-exact
app.post(
"/webhooks/invocore",
express.raw({ type: "application/json" }),
(req, res) => {
const signatureHeader = req.header("X-Invocore-Signature") || "";
if (!signatureHeader.startsWith("sha256=")) return res.status(401).end();
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const received = signatureHeader.slice("sha256=".length);
const sigMatch =
expected.length === received.length &&
crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(received, "hex")
);
if (!sigMatch) return res.status(401).end();
const event = req.header("X-Invocore-Event");
const payload = JSON.parse(req.body.toString("utf8"));
// ... handle payload.data ...
res.status(200).end();
}
);
<?php
$webhookSecret = getenv('INVOCORE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_INVOCORE_SIGNATURE'] ?? '';
if (strpos($signatureHeader, 'sha256=') !== 0) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $rawBody, $webhookSecret);
$received = substr($signatureHeader, strlen('sha256='));
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$event = $_SERVER['HTTP_X_INVOCORE_EVENT'] ?? '';
$payload = json_decode($rawBody, true);
// ... handle $payload['data'] ...
http_response_code(200);
Retry behavior & idempotency
Any non-2xx response (or network failure) triggers up to three retries: after 5 seconds, 25 seconds, and 125 seconds. The X-Invocore-Delivery header stays identical across all attempts — use it to dedup on your side and keep handlers idempotent. After three failed attempts the delivery stays in the failed state; you can manually re-fire it from the admin panel.
Ready to set it up?
Webhooks take less than five minutes to wire up from the admin panel — visual builder, Slack / Zapier / Teams / Make templates, and a built-in test endpoint included.