Webhooks
Echtzeit-Benachrichtigungen über Batch-Statusänderungen
Übersicht
Webhooks ermöglichen es Ihrer Anwendung, automatisch über Ereignisse in der SteuerMappePro API informiert zu werden. Anstatt kontinuierlich den Status eines Batches abzufragen (Polling), sendet die API eine HTTP POST-Anfrage an einen von Ihnen konfigurierten Endpunkt, sobald ein Ereignis eintritt.
Event-Driven
Sofortige Benachrichtigung bei Statusänderungen
Effizient
Kein Polling notwendig, spart API-Anfragen
Sicher
HMAC-Signatur zur Verifizierung der Absender
Zuverlässig
Automatische Wiederholung bei Fehlern
Vorteile von Webhooks
- Echtzeit-Updates: Erhalten Sie sofort Benachrichtigungen, wenn ein Batch abgeschlossen ist
- Reduzierter API-Traffic: Keine kontinuierliche Statusabfrage notwendig
- Automatisierung: Triggern Sie nachfolgende Prozesse automatisch
- Skalierbarkeit: Verarbeiten Sie viele parallele Batches effizient
Ereignistypen
Die SteuerMappePro API sendet Webhook-Benachrichtigungen für folgende Ereignisse:
| Ereignis | Beschreibung | Wann wird es ausgelöst? |
|---|---|---|
| batch.queued | Batch wurde erstellt | Direkt nach erfolgreicher Batch-Erstellung |
| batch.processing | Verarbeitung gestartet | Wenn die Dokumentenverarbeitung beginnt |
| batch.completed | Verarbeitung erfolgreich | Wenn alle Dokumente erfolgreich verarbeitet wurden |
| batch.failed | Verarbeitung fehlgeschlagen | Bei einem kritischen Fehler während der Verarbeitung |
Webhook-Payload Format
Alle Webhook-Benachrichtigungen werden als HTTP POST-Anfragen mit einem JSON-Body gesendet:
Beispiel: batch.completed Ereignis
{
"id": "evt_001",
"type": "batch.completed",
"created_at": "2025-10-06T10:10:00Z",
"batch": {
"id": "b_abc123",
"status": "completed",
"project_id": "prj_123",
"mandant_id": "m_123"
},
"result": {
"download_url": "https://files.partner.example/presigned/result.zip",
"file_size_bytes": 5242880,
"sha256": "a3b5c7d9e1f2..."
},
"usage": {
"pages": 38,
"documents": 12,
"total_duration_seconds": 245
},
"billing": {
"partner_code": "ABC123",
"snapshot_id": "pcs_789",
"total_cost_eur": "17.34"
}
}Beispiel: batch.failed Ereignis
{
"id": "evt_002",
"type": "batch.failed",
"created_at": "2025-10-06T10:15:00Z",
"batch": {
"id": "b_abc456",
"status": "failed",
"project_id": "prj_123",
"mandant_id": "m_456"
},
"error": {
"code": "CONVERSION_FAILED",
"message": "Dokument konnte nicht konvertiert werden",
"details": "Unsupported file format: .xyz"
}
}Payload-Felder
| Feld | Typ | Beschreibung |
|---|---|---|
| id | string | Eindeutige Event-ID (evt_...) |
| type | string | Ereignistyp (z.B. "batch.completed") |
| created_at | ISO 8601 | Zeitstempel des Ereignisses |
| batch | object | Batch-Informationen (ID, Status, etc.) |
| result | object? | Verarbeitungsergebnis (nur bei batch.completed) |
| error | object? | Fehlerinformationen (nur bei batch.failed) |
HMAC-Signaturverifizierung
Jede Webhook-Anfrage enthält eine HMAC-SHA256-Signatur im Header X-SteuerMappePro-Signature. Verifizieren Sie diese Signatur, um sicherzustellen, dass die Anfrage tatsächlich von SteuerMappePro stammt.
Sicherheitshinweis
Header-Format
Signatur-Verifizierung (TypeScript/Node.js)
import crypto from 'crypto';
export async function verifyWebhookSignature(
request: Request,
webhookSecret: string
): Promise<boolean> {
// 1. Signatur aus Header extrahieren
const signature = request.headers.get('X-SteuerMappePro-Signature');
if (!signature) {
throw new Error('Fehlende Webhook-Signatur');
}
// 2. Request Body lesen
const body = await request.text();
// 3. Erwartete Signatur berechnen
const expectedSignature = `sha256=${crypto
.createHmac('sha256', webhookSecret)
.update(body)
.digest('hex')}`;
// 4. Signaturen vergleichen (timing-safe)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}Webhook-Secret
webhookSecret wird bei der Webhook-Registrierung generiert. Sie können ihn entweder selbst beim Erstellen eines Batches mitgeben (webhook_secret Feld) oder von der API generieren lassen.Wiederholungsrichtlinie
Falls Ihr Webhook-Endpunkt nicht erreichbar ist oder einen Fehler zurückgibt (HTTP Status > 299), versucht die SteuerMappePro API die Zustellung automatisch erneut.
Exponential Backoff-Strategie
| Versuch | Wartezeit | Gesamtzeit |
|---|---|---|
| 1. Versuch | Sofort | - |
| 2. Versuch | 1 Minute | +1 min |
| 3. Versuch | 5 Minuten | +6 min |
| 4. Versuch | 30 Minuten | +36 min |
| 5. Versuch (Final) | 2 Stunden | +2h 36 min |
Maximale Versuche
Erfolgreiche Webhook-Zustellung
Ihr Endpunkt sollte mit einem HTTP-Statuscode 200-299 antworten, um eine erfolgreiche Zustellung zu signalisieren. Der Response-Body wird ignoriert.
Sicherheits-Best-Practices
Signatur-Verifizierung
- ✓ Immer die HMAC-Signatur verifizieren
- ✓ Timing-safe Vergleich verwenden (z.B. crypto.timingSafeEqual)
- ✓ Unbekannte/ungültige Anfragen mit 401 ablehnen
HTTPS verwenden
- ✓ Webhook-Endpunkt muss über HTTPS erreichbar sein
- ✓ Gültiges TLS-Zertifikat verwenden (Let's Encrypt empfohlen)
- ✓ TLS 1.2 oder höher
Idempotenz
- ✓ Webhooks können mehrfach zugestellt werden (bei Retries)
- ✓ Verwenden Sie die Event-ID für Duplikatserkennung
- ✓ Implementieren Sie idempotente Verarbeitung
Schnelle Antwortzeiten
- ✓ Webhook-Endpunkt sollte innerhalb von 10 Sekunden antworten
- ✓ Zeitintensive Verarbeitung asynchron durchführen (z.B. Job Queue)
- ✓ Sofort mit 200 OK antworten, dann verarbeiten
Code-Beispiele
Kompletter Webhook-Endpunkt (TypeScript/Node.js)
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
// Webhook-Secret (aus Umgebungsvariable)
const WEBHOOK_SECRET = process.env.STEUERMAPPE_WEBHOOK_SECRET!;
// Signatur verifizieren
function verifySignature(body: string, signature: string): boolean {
const expectedSignature = `sha256=${crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Webhook-Handler
export async function POST(request: NextRequest) {
try {
// 1. Signatur aus Header extrahieren
const signature = request.headers.get('X-SteuerMappePro-Signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
// 2. Request Body lesen
const body = await request.text();
// 3. Signatur verifizieren
if (!verifySignature(body, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// 4. Payload parsen
const event = JSON.parse(body);
// 5. Event-Typ prüfen und verarbeiten
switch (event.type) {
case 'batch.completed':
await handleBatchCompleted(event);
break;
case 'batch.failed':
await handleBatchFailed(event);
break;
case 'batch.processing':
await handleBatchProcessing(event);
break;
default:
console.log(`Unbekannter Event-Typ: ${event.type}`);
}
// 6. Erfolgreiche Antwort
return NextResponse.json({ received: true }, { status: 200 });
} catch (error) {
console.error('Webhook-Fehler:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// Event-Handler
async function handleBatchCompleted(event: any) {
console.log(`Batch ${event.batch.id} abgeschlossen!`);
console.log(`Download-URL: ${event.result.download_url}`);
// TODO: Ergebnis herunterladen und weiterverarbeiten
// z.B. in DATEV importieren, E-Mail versenden, etc.
}
async function handleBatchFailed(event: any) {
console.error(`Batch ${event.batch.id} fehlgeschlagen!`);
console.error(`Fehler: ${event.error.message}`);
// TODO: Fehlerbehandlung, z.B. Admin benachrichtigen
}
async function handleBatchProcessing(event: any) {
console.log(`Batch ${event.batch.id} wird verarbeitet...`);
// Optional: UI aktualisieren, Fortschritt anzeigen
}Idempotente Verarbeitung mit Duplikatserkennung
import { prisma } from '@/lib/prisma';
async function handleBatchCompleted(event: any) {
// Prüfen ob Event bereits verarbeitet wurde
const existingEvent = await prisma.webhookEvent.findUnique({
where: { eventId: event.id }
});
if (existingEvent) {
console.log(`Event ${event.id} bereits verarbeitet, überspringe...`);
return;
}
// Event verarbeiten
await processResult(event.result.download_url);
// Event als verarbeitet markieren
await prisma.webhookEvent.create({
data: {
eventId: event.id,
eventType: event.type,
batchId: event.batch.id,
processedAt: new Date()
}
});
}Webhooks lokal testen
Für lokale Entwicklung und Tests gibt es mehrere Möglichkeiten, Webhooks zu empfangen:
Option 1: ngrok
Erstellen Sie einen sicheren Tunnel zu Ihrem lokalen Server:
# ngrok installieren (https://ngrok.com)
# Tunnel zu localhost:3000 erstellen
ngrok http 3000
# Ausgabe:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
# Webhook-URL in SteuerMappePro konfigurieren:
# https://abc123.ngrok.io/api/webhooks/steuermappeOption 2: webhook.site
Für schnelle Tests ohne Code können Sie webhook.site verwenden:
- 1. Besuchen Sie https://webhook.site
- 2. Kopieren Sie die generierte URL
- 3. Verwenden Sie diese URL als webhook_url beim Batch-Erstellen
- 4. Sehen Sie eingehende Webhooks in Echtzeit im Browser
Option 3: Lokaler Test-Server
// test-webhook-server.ts
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
console.log('Webhook empfangen:');
console.log('Headers:', req.headers);
console.log('Body:', JSON.stringify(req.body, null, 2));
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook-Test-Server läuft auf http://localhost:3000');
});
// Starten:
// npx ts-node test-webhook-server.tsngrok-Empfehlung
Webhook registrieren
Sie können Webhooks auf zwei Arten registrieren:
Option 1: Bei Batch-Erstellung
Geben Sie webhook_url und optional webhook_secret beim Erstellen eines Batches an:
{
"batch_name": "Q4 Steuererklärungen",
"documents": [...],
"webhook_url": "https://api.ihr-system.de/webhooks/steuermappe",
"webhook_secret": "ihr_geheimer_webhook_key"
}Option 2: Persistente Webhook-Konfiguration
Konfigurieren Sie einen Webhook dauerhaft für Ihren API-Client (erfordert webhooks:manage Scope):
POST /v1/webhooks
Authorization: Bearer {access_token}
{
"url": "https://api.ihr-system.de/webhooks/steuermappe",
"events": ["batch.completed", "batch.failed"],
"secret": "ihr_geheimer_webhook_key"
}Webhook-Secret generieren
webhook_secret angeben, generiert die API automatisch einen sicheren Schlüssel und gibt ihn in der Response zurück.Fehlerbehebung
Webhook wird nicht zugestellt
- ✓ Prüfen Sie, ob Ihr Endpunkt über HTTPS erreichbar ist
- ✓ Stellen Sie sicher, dass Ihr Server innerhalb von 10 Sekunden antwortet
- ✓ Überprüfen Sie Firewall-Regeln und IP-Whitelisting
- ✓ Sehen Sie in den Webhook-Logs in der Admin-Konsole nach
Signatur-Verifizierung schlägt fehl
- ✓ Verwenden Sie den korrekten webhook_secret
- ✓ Berechnen Sie den HMAC über den rohen Request Body (nicht geparst)
- ✓ Verwenden Sie SHA-256 Hashing-Algorithmus
- ✓ Vergleichen Sie mit timing-safe Methode (crypto.timingSafeEqual)
Doppelte Webhook-Zustellungen
- ✓ Dies ist normales Verhalten bei Netzwerkproblemen oder Timeouts
- ✓ Implementieren Sie Duplikatserkennung anhand der
id - ✓ Stellen Sie sicher, dass Ihre Verarbeitung idempotent ist
Support
Sicherheit
Webhooks werden mit einer HMAC-Signatur versehen, damit Sie die Echtheit der Anfragen verifizieren können.
Signatur-Header
X-SMP-Signature: HMAC-SHA256 übertimestamp + '.' + rawBodyX-SMP-Timestamp: UNIX-Sekunden (Zeitfenster ±5 Minuten)X-SMP-Event-Id: Eindeutige Ereignis-ID (zur Replay-Abwehr)
Beispiel: TypeScript-Verifizierung
import crypto from 'crypto';
function timingSafeEqual(a: string, b: string) {
const ab = Buffer.from(a);
const bb = Buffer.from(b);
if (ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
export function verifyWebhook({
bodyRaw,
signature,
timestamp,
secret,
}: {
bodyRaw: string;
signature: string;
timestamp: string;
secret: string;
}) {
// 1) Timestamp window check (±5 minutes)
const now = Math.floor(Date.now() / 1000);
const ts = parseInt(timestamp, 10);
if (Number.isNaN(ts) || Math.abs(now - ts) > 300) {
throw new Error('Timestamp outside allowed window');
}
// 2) Compute expected signature
const payload = timestamp + '.' + bodyRaw;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!timingSafeEqual(expected, signature)) {
throw new Error('Invalid webhook signature');
}
}
Retries
Bei temporären Fehlern (z. B. 5xx) wird mit exponentiellem Backoff erneut zugestellt (mehrere Versuche). Verwenden SieX-SMP-Event-Id, um Doppellieferungen idempotent zu verarbeiten.