Webhook-Events
Webhooks anlegen, die Events, die kanman für Stories, Runs, Gates, Entscheidungen und Teams sendet, Signaturen prüfen sowie die Zustell- und Wiederholungsregeln.
Webhooks schicken eine signierte HTTP-Anfrage an Ihren Server, wenn in kanman etwas passiert: Eine Story wird bereit, ein Run endet oder schlägt fehl, ein Gate schlägt fehl, eine Entscheidung wartet auf eine Antwort, ein Teambericht erscheint. Damit posten Sie in Ihre eigenen Chat-Werkzeuge, aktualisieren ein Dashboard oder starten eigene Automatisierungen.
Webhooks gehören zum Workspace. Owner und Admins legen sie an und verwalten sie; Mitglieder sehen sie nicht.
Webhook anlegen
- Öffnen Sie Einstellungen, Webhooks (
/<workspace>/settings/webhooks). - Klicken Sie auf Webhook hinzufügen.
- Geben Sie einen Name und die Endpoint-URL Ihres Servers ein.
- Wählen Sie unter Events, welche Events der Webhook erhalten soll. Sie sind gruppiert nach Stories, Runs und Pull Requests, Outcome Gate, Entscheidungen sowie Teams und Berichten. Ein neuer Webhook beginnt mit Run abgeschlossen, Run fehlgeschlagen oder gestoppt und Entscheidung wartet.
- Klicken Sie auf Webhook hinzufügen.
kanman erzeugt für jeden Webhook ein Signatur-Secret (es beginnt mit whsec_). Mit Anzeigen sehen Sie es, mit Neu generieren ersetzen Sie es; Ihr Empfänger braucht es, um Signaturen zu prüfen. Der Schalter an jedem Webhook schaltet die Zustellung ein oder aus. Ein Klick auf den Namen eines Webhooks ändert Name, Endpunkt oder Events.
Events
| Event | Gesendet, wenn |
|---|---|
story.created |
Eine Story in einem Team erscheint: Ein Issue wurde aus Ihrem Tracker übernommen, oder ein freigegebener Story-Entwurf wurde in den Tracker geschrieben. |
story.ready |
Eine Story in die Spalte Bereit des Teams kommt. |
run.started |
kanman mit der Arbeit an einer Story beginnt (auch bei jedem neuen Versuch). |
run.stage_changed |
Ein Run in eine andere Phase wechselt: Planung, Umsetzung, Prüfung, Review, fertig. |
run.completed |
Ein Versuch eines Runs seine Arbeit beendet. |
run.failed |
Ein Versuch fehlschlägt, in ein Zeitlimit läuft oder gestoppt wird. |
pr.opened |
kanman den Pull Request eines Runs öffnet. |
gate.passed |
Ein Gate des Outcome Gates besteht, zum Beispiel die Clean-Room-Prüfung oder der Reviewer. |
gate.failed |
Ein Gate fehlschlägt oder nicht laufen kann. |
decision.created |
kanman eine Entscheidung von einem Menschen braucht. |
decision.resolved |
Jemand eine Entscheidung beantwortet hat, in der App, in Slack, über den MCP-Server oder die REST-API. |
report.published |
Der Wochenbericht eines Teams fertig ist. |
team.paused |
Ein Team pausiert wird. |
team.resumed |
Ein pausiertes Team fortgesetzt wird. |
test |
Sie auf Test senden klicken. Nicht abonnierbar. |
Format der Anfrage
Jede Zustellung ist ein HTTP-POST mit JSON-Body:
{
"id": "6f1d2c80-4c1e-4f3a-9b7e-2a1d8c5e9f10",
"event": "run.failed",
"timestamp": "2026-10-01T09:16:56.512Z",
"workspaceId": "a1b2c3d4-0000-4000-8000-000000000001",
"attempt": 1,
"data": {
"team": { "id": "...", "slug": "sandbox-team", "name": "Sandbox Team", "keyPrefix": "SBX" },
"story": {
"id": "...", "key": "SBX-12", "title": "Export invoices as CSV", "readinessScore": 100,
"tracker": { "platform": "github", "key": "#12", "url": "https://github.com/acme/app/issues/12" }
},
"run": {
"key": "run-7f3k", "attempt": 2, "status": "failed", "stage": "verifying", "executor": "claude-code",
"costCents": 140, "budgetCents": 400, "failureClass": "gate", "prUrl": null, "prNumber": null,
"startedAt": "2026-10-01T09:02:10.000Z", "completedAt": "2026-10-01T09:16:56.000Z"
},
"error": "Verification failed"
}
}
id ist die Zustell-ID; sie bleibt bei Wiederholungen gleich, und attempt zählt die Versuche. timestamp ist der Zeitpunkt des Ereignisses, nicht der Zustellung.
Was data enthält, hängt vom Event ab:
| Events | Felder in data |
|---|---|
story.* |
team, story |
run.* |
team, story, run; run.stage_changed ergänzt previousStage, run.failed ergänzt error |
pr.opened |
team, story, run, pullRequest (url, number) |
gate.* |
team, story, gate (name, verdict, reason, attempt, runKey, finishedAt) |
decision.* |
team, story, decision (key, kind, authority, title, status, recommendation, options, runKey, resolution, resolvedVia, resolvedBy, resolvedAt, expiresAt); decision.resolved ergänzt result |
report.published |
team, report (periodKey, periodStart, periodEnd, summary, metrics) |
team.* |
team, pausedAt, by |
story ist null bei Events ohne Story, zum Beispiel bei einer Entscheidung über das ganze Team. Behandeln Sie unbekannte Felder als optional: kanman kann Felder ergänzen, entfernt oder benennt sie aber nicht um.
| Header | Beschreibung |
|---|---|
Content-Type |
application/json |
User-Agent |
kanman-webhooks/1 |
X-Kanman-Event |
Event-Typ, zum Beispiel run.failed. |
X-Kanman-Delivery |
Zustell-ID (UUID), dieselbe wie id im Body. Nutzen Sie sie, um Duplikate zu ignorieren. |
X-Kanman-Timestamp |
Unix-Zeit in Sekunden, zu der dieser Versuch signiert wurde. |
X-Kanman-Signature |
sha256= gefolgt von der hexadezimalen HMAC-SHA256-Signatur. |
Signatur prüfen
Die Signatur ist HMAC-SHA256 mit Ihrem Webhook-Secret über den String <X-Kanman-Timestamp>.<roher Request-Body>. Berechnen Sie sie über die rohen Bytes, die Sie empfangen haben, vor jedem JSON-Parsen. Wenn Sie den Body parsen und neu serialisieren, ändern sich Leerzeichen und Schlüsselreihenfolge, und die Signatur passt nicht mehr. Lehnen Sie außerdem Zustellungen ab, deren Zeitstempel älter als fünf Minuten ist, damit eine mitgeschnittene Anfrage nicht wiederholt werden kann.
Node.js mit Express:
const crypto = require('crypto')
const express = require('express')
const app = express()
const SECRET = process.env.KANMAN_WEBHOOK_SECRET
// Keep the raw body: the signature covers the exact bytes kanman sent
app.post('/kanman', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Kanman-Timestamp')
const signature = req.get('X-Kanman-Signature') || ''
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${req.body.toString('utf8')}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
const valid = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
if (!fresh || !valid) return res.status(401).send('invalid signature')
const { event, data } = JSON.parse(req.body)
res.status(200).send('ok')
handle(event, data) // do the real work after answering
})
Python mit Flask:
import hashlib
import hmac
import json
import os
import time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["KANMAN_WEBHOOK_SECRET"].encode()
@app.post("/kanman")
def kanman_webhook():
timestamp = request.headers.get("X-Kanman-Timestamp", "0")
signature = request.headers.get("X-Kanman-Signature", "")
raw = request.get_data() # raw bytes, not request.json
expected = "sha256=" + hmac.new(SECRET, timestamp.encode() + b"." + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(timestamp)) > 300 or not hmac.compare_digest(expected, signature):
return "invalid signature", 401
payload = json.loads(raw)
print(payload["event"], payload["data"])
return "ok", 200
Zustellregeln
- kanman sendet ein Event wenige Sekunden, nachdem es passiert ist.
- Antworten Sie innerhalb von 10 Sekunden mit einem
2xx-Status. Erledigen Sie langsame Arbeit erst nach der Antwort. Weiterleitungen werden nicht verfolgt. - Eine fehlgeschlagene Zustellung wird bis zu viermal wiederholt: nach 1 Minute, 5 Minuten, 30 Minuten und 2 Stunden. Netzwerkfehler, Zeitüberschreitungen sowie Antworten mit
408,429und5xxwerden wiederholt; andere4xx-Antworten gelten sofort als endgültiger Fehlschlag. - Nach 5 endgültig fehlgeschlagenen Zustellungen in Folge wird der Webhook pausiert und zeigt Pausiert mit einer kurzen Erklärung; noch wartende Zustellungen für ihn entfallen. Beheben Sie den Endpunkt und schalten Sie den Webhook unter Einstellungen, Webhooks mit seinem Schalter wieder ein. Das Einschalten setzt den Fehlerzähler zurück.
- Jedes Event wird mindestens einmal zugestellt. Nutzen Sie
X-Kanman-Delivery(oderid), um eine bereits verarbeitete Zustellung zu ignorieren. - Zustellungen verschiedener Events können in anderer Reihenfolge ankommen. Ordnen Sie sie über
timestamp.
Test
Klicken Sie an einem Webhook auf Test senden, um ein test-Event an Ihren Endpunkt zu schicken. Die App zeigt an, ob die Zustellung geklappt hat. Der Test-Body sieht so aus:
{
"id": "0b9d4a6e-8c2f-4d1a-9e3b-7f6a5c4d3e2b",
"event": "test",
"timestamp": "2026-10-01T09:00:00.000Z",
"workspaceId": "a1b2c3d4-0000-4000-8000-000000000001",
"attempt": 1,
"data": {
"message": "This is a test delivery from kanman",
"webhookId": "3f0c2b9e-6a51-4c1e-9f0a-2d7b8e41c5aa",
"webhookName": "Ops alerts"
}
}
Ein Test wird einmal gesendet und nicht wiederholt. Für lokale Tests machen Sie Ihren Rechner über einen Tunnel erreichbar und tragen die öffentliche HTTPS-URL als Endpunkt ein.
Um einen Webhook zu löschen, klicken Sie auf seinen Löschen-Button und bestätigen mit Webhook löschen.
Verwandte Seiten
Zuletzt aktualisiert: January 1, 0001
kanman öffnen