Darstellung
Webhooks
Webhooks benachrichtigen dein System, sobald in Work7 etwas passiert: ein Rapport wurde abgeschlossen, eine Rechnung bezahlt, eine Anfrage angelegt. Du registrierst eine https-URL und die Events, die dich interessieren. Work7 sendet dann einen signierten POST.
Einrichten
- Einstellungen › Integrationen › API & Webhooks, Abschnitt Ausgehende Webhooks.
- Name, Ziel-URL (nur
https) und Events wählen. Wildcards wierapport.*sind erlaubt. - Webhook anlegen. Das Secret (
whsec_…) wird einmalig angezeigt und wird zum Prüfen der Signatur gebraucht.
Webhooks lassen sich pausieren und wieder aktivieren. Ereignisse während einer Pause werden nicht nachgeliefert.
Events
Events sind nach Bereich benannt. Es werden nur Events von Bereichen gesendet, die für den Betrieb freigeschaltet sind. Die aktuell verfügbaren Events zeigt die Verwaltung beim Anlegen an.
| Bereich | Events (Auswahl) |
|---|---|
| Projekte, Kontakte | project.created, project.updated, kontakt.created, kontakt.updated |
| Anfragen | anfrage.created, anfrage.updated, anfrage.status_changed |
| Rapport | rapport.created, rapport.completed, rapport.sent |
| Angebote | offer.created, offer.sent, offer.accepted |
| Rechnungen | invoice.created, invoice.sent, invoice.paid, invoice.exported |
| Zeiterfassung | time_entry.created, time_entry.updated |
| Termine | appointment.created, appointment.updated |
Ein Event-Name besteht aus <entität>.<aktion>. Neue Aktionen können hinzukommen, dein Empfänger sollte unbekannte Events ignorieren, nicht ablehnen.
Zustellung
http
POST https://deine-domain.example/work7-webhook
Content-Type: application/json
User-Agent: Work7-Webhooks/1.0
X-Work7-Event: rapport.completed
X-Work7-Delivery: 8123
X-Work7-Timestamp: 1757671200
X-Work7-Signature: sha256=5f1c…e9json
{
"id": "0d9a8f2e-3b6c-4e1a-9f21-6d1f0c4b7a10",
"type": "rapport.completed",
"companyId": "muster-malerei",
"createdAt": "2026-09-12T10:00:00.000Z",
"data": {
"entity": "rapport",
"action": "completed",
"entityId": 4711,
"userId": "18",
"data": { "rapport_number": "RP-2026-0087" }
}
}idist je Ereignis eindeutig. Mehrere Webhooks erhalten dasselbeid. Nutze es, um Doppelverarbeitung zu vermeiden.- Die Nutzlast enthält Kennungen und die geänderten Felder, keine vollständigen Dokumente. Details holst du bei Bedarf über die API, zum Beispiel
GET /rapports/4711. - Antworte innerhalb von 10 Sekunden mit einem 2xx-Status. Alles andere gilt als Fehlschlag.
Signatur prüfen
Jede Zustellung ist mit HMAC-SHA256 über "<timestamp>.<body>" signiert, Schlüssel ist das Webhook-Secret. Prüfe immer die Signatur und das Alter des Zeitstempels (empfohlen: maximal fünf Minuten), bevor du das Ereignis verarbeitest.
js
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.WORK7_WEBHOOK_SECRET
// Rohen Body behalten: die Signatur gilt für die exakten Bytes.
app.post('/work7-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.header('X-Work7-Timestamp') ?? ''
const signature = req.header('X-Work7-Signature') ?? ''
const body = req.body.toString('utf8')
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp))
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return res.status(400).send('stale')
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(`${timestamp}.${body}`).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signature)
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).send('bad signature')
const event = JSON.parse(body)
// Idempotenz: event.id merken, bereits gesehene Events überspringen.
if (event.type === 'rapport.completed') {
// ... eigene Verarbeitung, z. B. Rapport per API laden
}
res.status(204).end()
})php
<?php
$secret = getenv('WORK7_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WORK7_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WORK7_SIGNATURE'] ?? '';
if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit('stale');
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($body, true);
// Idempotenz: $event['id'] speichern, Duplikate überspringen.
if ($event['type'] === 'invoice.paid') {
// ... eigene Verarbeitung
}
http_response_code(204);Wiederholungen
Antwortet dein Endpunkt nicht mit 2xx oder nicht innerhalb von 10 Sekunden, versucht Work7 es erneut, insgesamt bis zu fünf Mal mit wachsendem Abstand:
| Versuch | Wartezeit danach |
|---|---|
| 1 | 1 Minute |
| 2 | 5 Minuten |
| 3 | 15 Minuten |
| 4 | 60 Minuten |
| 5 | keine, Status dead |
Den Zustellstatus je Ereignis siehst du in der Verwaltung unter Letzte Zustellungen (Status, HTTP-Code, Fehlertext, Versuche). Ereignisse mit Status dead werden nicht automatisch erneut gesendet.
Testen
- Lokal lässt sich der Endpunkt mit einem Tunnel wie
ngrokodercloudflarederreichbar machen. Die URL musshttpssein. - Ein erstes Ereignis erzeugst du am einfachsten in der App, etwa durch Abschließen eines Test-Rapports.
- Für Unit-Tests der Signaturprüfung reicht es,
"<timestamp>.<body>"mit dem Secret zu signieren, wie oben gezeigt.