Polyhedral can send real-time HTTP POST requests to your server when events happen in your games — chat messages, dice rolls, notes, encounters, and more.
Every delivery is a JSON POST with three top-level fields:
{
"event": "note.created",
"timestamp": "2026-01-30T12:00:00.000Z",
"data": {
"note_id": "abc-123",
"title": "Session Log"
}
}Every delivery includes these custom headers:
X-Polyhedral-Event — event type, e.g. note.createdX-Polyhedral-Signature — HMAC-SHA256 signature: sha256=<hex>X-Polyhedral-Delivery — unique delivery ID (UUID)User-Agent — Polyhedral-Webhooks/1.0Subscribe to any combination of events when creating or editing a webhook.
chat.message.createdA new chat message was sent (user or AI).
{ "message_id": "uuid", "role": "user" | "assistant" }dice.rolledDice were rolled.
{ "formula": "2d6+3", "reason": "Attack roll", "result": 11 }note.createdA note was created.
{ "note_id": "uuid", "title": "Session Log" }note.updatedA note was updated.
{ "note_id": "uuid", "title": "Session Log" }encounter.createdAn encounter was created.
{ "encounter_id": "uuid", "name": "Goblin Ambush" }encounter.updatedAn encounter was updated.
{ "encounter_id": "uuid", "name": "Goblin Ambush" }game.member.joinedA new member joined the game.
{ "member_id": "uuid", "user_id": "uuid", "display_name": "Player" }resource.added_to_gameA resource was added to the game library.
{ "resource_id": "uuid", "game_resource_id": "uuid" }table.createdA rollable table was created.
{ "table_id": "uuid", "name": "Random Encounters" }table.rolledA rollable table was rolled on.
{ "table_id": "uuid", "name": "Random Encounters", "result": "Owlbear" }table.updatedA rollable table was updated.
{ "table_id": "uuid", "name": "Random Encounters" }Use your webhook signing secret to verify that deliveries actually come from Polyhedral. Compute the HMAC-SHA256 of the raw request body and compare it to the X-Polyhedral-Signature header.
import crypto from 'crypto';
function verifySignature(payload, secret, signature) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return signature === `sha256=${expected}`;
}
// In your handler:
const sig = req.headers['x-polyhedral-signature'];
const body = JSON.stringify(req.body);
if (!verifySignature(body, process.env.WEBHOOK_SECRET, sig)) {
return res.status(401).send('Invalid signature');
}import hmac, hashlib
def verify_signature(payload: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, f"sha256={expected}")
# In your handler:
sig = request.headers.get("X-Polyhedral-Signature", "")
if not verify_signature(request.data, WEBHOOK_SECRET, sig):
abort(401)If your endpoint returns a 5xx error or times out (10 seconds), Polyhedral will retry the delivery up to 5 more times with increasing delays:
4xx responses are not retried — they indicate a client-side problem that retrying won't fix.
Manage your webhooks in Connected Apps from your avatar menu. For programmatic access, see the MCP documentation.