Developer cookbook

Webhooks cookbook - verify signatures, ACK fast.

Every webhook we deliver carries X-CodeB-Signature: sha256=<hex> where the HMAC-SHA256 is computed over the exact raw request body with the per-endpoint secret you set at registration in webhooks-admin.html. Additional headers X-CodeB-Event, X-CodeB-Delivery-Id, and X-CodeB-Attempt travel with each request. European Digital Identity Wallet session events flow through the same dispatcher.

Signature. Header X-CodeB-Signature: sha256=<lowercase-hex>. HMAC input is the raw request body bytes. Compare with a constant-time helper. Live events today: call.started, call.answered, call.ended, call.ice.connected, call.transferred, transcript.saved, outbound-ai.finished, sfu.publisher.connected, sfu.subscriber.connected. Reserved for future emission: 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 - these are advertised in the picker in webhooks-admin.html and will fire once the room-level SFU emitters land. Subscribing to them today is safe but produces no traffic yet.

Framework matrix

Framework / languageLibraryRecipeSnippet
Node.js (Express)raw-body + timingSafeEqualOpenwebhook-receive-nodejs.js
Python (FastAPI)hmac.compare_digestOpenwebhook-receive-python.py
PHP (raw handler)hash_equalsOpenwebhook-receive-php.php
ASP.NET CoreFixedTimeEqualsOpenwebhook-receive-dotnet.cs
Go (net/http)hmac.EqualOpenwebhook-receive-go.go

Node.js (Express) Recipe

raw-body + timingSafeEqual. Download raw .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) Recipe

hmac.compare_digest. Download raw .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) Recipe

hash_equals. Download raw .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 Recipe

FixedTimeEquals. Download raw .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) Recipe

hmac.Equal. Download raw .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)) }

Test it

Use the Send test event button in webhooks-admin.html. It posts a canned payload of any registered event to your endpoint with a correct signature so you can validate the receiver end-to-end.

FAQ

See structured answers in the schema block above.