Webhooks
Event catalog, payload envelope, signature verification, delivery semantics, and SSRF hardening.
Configuring a webhook#
Repository- and organization-level webhooks (Settings → Webhooks) subscribe to a set of events and POST a JSON payload to a configured URL on each occurrence.
Event catalog#
PUSH, PULL_REQUEST_OPENED, PULL_REQUEST_UPDATED, PULL_REQUEST_MERGED, ISSUE_CREATED, ISSUE_CLOSED, RELEASE_CREATED, PIPELINE_COMPLETED.
Payload envelope#
For a generic endpoint, the request body is:
{
"event": "PULL_REQUEST_OPENED",
"payload": { "...": "event-specific fields" },
"deliveredAt": "2026-01-01T00:00:00.000Z"
}Headers and signature verification#
| Header | Value |
|---|---|
X-KHUB-Event | The event name, matching payload.event. |
X-KHUB-Delivery | A unique ID for this delivery attempt series — stable across retries of the same logical delivery, usable for idempotency on the receiving end. |
X-KHUB-Signature-256 | sha256=<hex hmac> — an HMAC-SHA256 of the exact raw request body, keyed with the webhook's per-webhook secret (shown once at creation). |
Verify it (Node.js) before trusting a payload:
const crypto = require("crypto");
function isValid(rawBody, signatureHeader, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}Always compute the HMAC over the raw request body bytes, before any JSON parsing — re-serializing a parsed body can reorder keys or change whitespace and invalidate the comparison.
Delivery semantics#
- Up to 3 delivery attempts per event occurrence; a 3xx response is treated as a failure (webhook endpoints are not followed through redirects, to avoid signature-verification ambiguity about which host actually received the payload).
- Every attempt — including failures, response code, and response body — is recorded and viewable per-webhook, with a manual "redeliver" action for any past delivery.
- Outbound requests are validated against a private/link-local IP allowlist before being sent (an SSRF guard), so a webhook URL cannot be used to make KHUB's server issue a request to its own internal network or cloud metadata endpoint.
Chat-platform auto-formatting#
If a webhook's URL is recognized as a Slack or Discord incoming-webhook URL, the payload is automatically reformatted into that platform's native message shape instead of the generic envelope above — pointing a webhook at a Slack "Incoming Webhook" URL produces a readable chat message with no adapter service required.
