/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.created

Fired 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.sent

Fired 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.paid

Fired 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.overdue

Fired 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.updated

Fired 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.deleted

Fired 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.created

Fired 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.updated

Fired 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.deleted

Fired 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.received

Fired 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.overdue

Fired 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.uploaded

Fired 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.started

Fired 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.failed

Fired 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.requested

Fired 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.decided

Fired 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:

HeaderDescription
X-Invocore-EventEvent type (e.g. invoice.paid)
X-Invocore-SignatureHMAC-SHA256 in the form sha256=<hex>
X-Invocore-DeliveryUnique delivery ID (use for idempotency and dedup)
X-Invocore-TimestampISO-8601 timestamp of the first delivery attempt
User-AgentInvocore-Webhook/1.0
Content-Typeapplication/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).

Python (Flask)
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
Node.js (Express)
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
<?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.