Webhook-Kochbuch - Signaturen pruefen, schnell ACKen.
Jeder Webhook traegt X-CodeB-Signature: sha256=<hex> - HMAC-SHA256 ueber die exakten Roh-Bytes des Request-Bodies mit dem in webhooks-admin.html hinterlegten Endpoint-Secret. Zusaetzlich: X-CodeB-Event, X-CodeB-Delivery-Id, X-CodeB-Attempt. Session-Events der European Digital Identity Wallet fliessen ueber denselben Dispatcher.
Signatur. Header X-CodeB-Signature: sha256=<kleinbuchstaben-hex>. HMAC-Eingabe sind die Roh-Bytes des Request-Bodies. Vergleich in konstanter Zeit. Aktuell live: call.started, call.answered, call.ended, call.ice.connected, call.transferred, transcript.saved, outbound-ai.finished, sfu.publisher.connected, sfu.subscriber.connected. Fuer die Zukunft reserviert: room.cascade.promoted, room.cascade.demoted, room.cascade.failed, room.peer.joined, room.peer.left, room.bandwidth.degraded, sfu.offer.received, sfu.subscribe.received, room.client.preference.changed - im Picker in webhooks-admin.html sichtbar; feuern nach Landung der Room-SFU-Emitter. Abonnieren heute ist unschaedlich, erzeugt aber keinen Traffic.
Framework-Matrix
| Framework / Sprache | Bibliothek | Rezept | Snippet |
|---|---|---|---|
| Node.js (Express) | raw-body + timingSafeEqual | Anzeigen | webhook-receive-nodejs.js |
| Python (FastAPI) | hmac.compare_digest | Anzeigen | webhook-receive-python.py |
| PHP (raw handler) | hash_equals | Anzeigen | webhook-receive-php.php |
| ASP.NET Core | FixedTimeEquals | Anzeigen | webhook-receive-dotnet.cs |
| Go (net/http) | hmac.Equal | Anzeigen | webhook-receive-go.go |
Node.js (Express) Rezept
raw-body + timingSafeEqual. Rohdatei laden .js
// Receive + verify a CodeB webhook in Node.js (Express).
// Events include call.started, call.answered, call.ended, transcript.saved,
// call.ice.connected, call.transferred, outbound-ai.finished,
// sfu.publisher.connected, sfu.subscriber.connected.
// European Digital Identity Wallet sign-in events flow through the same
// dispatcher; verification is identical.
// [webhooks-cookbook 2026-07-23]
// npm install express
const express = require('express');
const crypto = require('crypto');
const SECRET = process.env.CODEB_WEBHOOK_SECRET; // per-tenant secret
const app = express();
// IMPORTANT: capture the RAW body -- HMAC is over the exact bytes on the
// wire, not the re-serialised JSON.
app.use('/webhook', express.raw({ type: '*/*' }));
app.post('/webhook', (req, res) => {
const got = req.header('X-CodeB-Signature') || ''; // "sha256=<hex>"
const body = req.body; // Buffer
const mac = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
const want = 'sha256=' + mac;
const ok = got.length === want.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
if (!ok) return res.status(401).send('bad signature');
const evt = req.header('X-CodeB-Event');
const delId = req.header('X-CodeB-Delivery-Id');
const payload = JSON.parse(body.toString('utf8'));
console.log(`[${delId}] ${evt}`, payload);
// ... process asynchronously; ACK within 5s.
res.status(200).send('ok');
});
app.listen(4000);
Python (FastAPI) Rezept
hmac.compare_digest. Rohdatei laden .python
# Receive + verify a CodeB webhook in Python (FastAPI).
# Events include call.started, call.answered, call.ended, transcript.saved,
# call.ice.connected, call.transferred, outbound-ai.finished,
# sfu.publisher.connected, sfu.subscriber.connected.
# European Digital Identity Wallet sign-in events flow through the same
# dispatcher; verification is identical.
# [webhooks-cookbook 2026-07-23]
# pip install fastapi uvicorn
import hmac, hashlib, os, json
from fastapi import FastAPI, Request, Header, HTTPException
SECRET = os.environ["CODEB_WEBHOOK_SECRET"].encode("utf-8")
app = FastAPI()
@app.post("/webhook")
async def webhook(
request: Request,
x_codeb_signature: str | None = Header(None),
x_codeb_event: str | None = Header(None),
x_codeb_delivery_id: str | None = Header(None),
):
raw = await request.body() # exact bytes; HMAC input
mac = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
want = f"sha256={mac}"
if not x_codeb_signature or not hmac.compare_digest(x_codeb_signature, want):
raise HTTPException(status_code=401, detail="bad signature")
payload = json.loads(raw.decode("utf-8"))
print(f"[{x_codeb_delivery_id}] {x_codeb_event}", payload)
# ... process asynchronously; ACK within 5s.
return {"ok": True}
# Run: uvicorn webhook_receive:app --port 4000
PHP (raw handler) Rezept
hash_equals. Rohdatei laden .php
<?php
// Receive + verify a CodeB webhook in PHP (raw handler).
// Events include call.started, call.answered, call.ended, transcript.saved,
// call.ice.connected, call.transferred, outbound-ai.finished,
// sfu.publisher.connected, sfu.subscriber.connected.
// European Digital Identity Wallet sign-in events flow through the same
// dispatcher; verification is identical.
// [webhooks-cookbook 2026-07-23]
//
// Wire this up as the endpoint you registered on
// https://<CODEB_TENANT_HOST>/webhooks-admin.html
$secret = getenv('CODEB_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // exact bytes; HMAC input
$got = $_SERVER['HTTP_X_CODEB_SIGNATURE'] ?? '';
$mac = hash_hmac('sha256', $raw, $secret);
$want = 'sha256=' . $mac;
if (!hash_equals($want, $got)) {
http_response_code(401);
echo 'bad signature';
exit;
}
$evt = $_SERVER['HTTP_X_CODEB_EVENT'] ?? '';
$delId = $_SERVER['HTTP_X_CODEB_DELIVERY_ID'] ?? '';
$payload = json_decode($raw, true);
error_log("[{$delId}] {$evt} " . json_encode($payload));
// ... process asynchronously; ACK within 5s.
http_response_code(200);
echo 'ok';
ASP.NET Core Rezept
FixedTimeEquals. Rohdatei laden .csharp
// Receive + verify a CodeB webhook in ASP.NET Core.
// Events include call.started, call.answered, call.ended, transcript.saved,
// call.ice.connected, call.transferred, outbound-ai.finished,
// sfu.publisher.connected, sfu.subscriber.connected.
// European Digital Identity Wallet sign-in events flow through the same
// dispatcher; verification is identical.
// [webhooks-cookbook 2026-07-23]
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string SECRET = Environment.GetEnvironmentVariable("CODEB_WEBHOOK_SECRET")
?? throw new InvalidOperationException("set CODEB_WEBHOOK_SECRET");
app.MapPost("/webhook", async (HttpRequest req) =>
{
// Read raw body -- HMAC is over the exact bytes.
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
byte[] raw = ms.ToArray();
string got = req.Headers["X-CodeB-Signature"].ToString();
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(SECRET));
byte[] hash = h.ComputeHash(raw);
string want = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(got),
Encoding.ASCII.GetBytes(want)))
return Results.Unauthorized();
string evt = req.Headers["X-CodeB-Event"].ToString();
string delId = req.Headers["X-CodeB-Delivery-Id"].ToString();
string payload = Encoding.UTF8.GetString(raw);
Console.WriteLine($"[{delId}] {evt} {payload}");
// ... process asynchronously; ACK within 5s.
return Results.Ok();
});
app.Run();
Go (net/http) Rezept
hmac.Equal. Rohdatei laden .go
// Receive + verify a CodeB webhook in Go (net/http).
// Events include call.started, call.answered, call.ended, transcript.saved,
// call.ice.connected, call.transferred, outbound-ai.finished,
// sfu.publisher.connected, sfu.subscriber.connected.
// European Digital Identity Wallet sign-in events flow through the same
// dispatcher; verification is identical.
// [webhooks-cookbook 2026-07-23]
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"log"
"net/http"
"os"
)
var secret = []byte(os.Getenv("CODEB_WEBHOOK_SECRET"))
func handler(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad body", 400)
return
}
got := r.Header.Get("X-CodeB-Signature")
m := hmac.New(sha256.New, secret)
m.Write(raw)
want := "sha256=" + hex.EncodeToString(m.Sum(nil))
if len(got) != len(want) || !hmac.Equal([]byte(got), []byte(want)) {
http.Error(w, "bad signature", 401)
return
}
evt := r.Header.Get("X-CodeB-Event")
delId := r.Header.Get("X-CodeB-Delivery-Id")
log.Printf("[%s] %s %s", delId, evt, string(raw))
// ... process asynchronously; ACK within 5s.
w.WriteHeader(200)
w.Write([]byte("ok"))
}
func main() {
http.HandleFunc("/webhook", handler)
log.Fatal(http.ListenAndServe(":4000", nil))
}
Selber testen
Nutzen Sie Send test event in webhooks-admin.html. Es feuert eine korrekt signierte, vordefinierte Payload jedes registrierten Events an Ihren Endpoint.
FAQ
Strukturierte Antworten im obigen Schema-Block.