Webhook reference
Event types, wire payload, signature verification, and the delivery contract of the decidio webhook system.
Overview
Webhooks let your system react to events in decidio in real time — without polling. Every outgoing request is signed with HMAC-SHA256, retried on failure, and delivered as canonical JSON.
Wire payload
Each event is delivered as a JSON object with four fields: event_type, timestamp, municipality_id, and data. The shape of data depends on the event type (see below).
Event types
Five event types are dispatched today.
submission.created
Payload fields
submission_id— stringreference_number— objecttopic_category_name— objecttopic_source— stringpriority_level— stringcreated_at— string
Example payload
{
"event_type": "submission.created",
"timestamp": "2026-01-01T12:00:00.000Z",
"municipality_id": "00000000-0000-0000-0000-0000000000aa",
"data": {
"submission_id": "00000000-0000-0000-0000-000000000001",
"reference_number": "SUB-2026-000001",
"topic_category_name": "Verkehr",
"topic_source": "citizen",
"priority_level": "normal",
"created_at": "2026-01-01T12:00:00.000Z"
}
}submission.status_changed
Payload fields
submission_id— stringreference_number— objectprevious_status— stringnew_status— stringreason— objectchanged_at— string
Example payload
{
"event_type": "submission.status_changed",
"timestamp": "2026-01-01T12:00:00.000Z",
"municipality_id": "00000000-0000-0000-0000-0000000000aa",
"data": {
"submission_id": "00000000-0000-0000-0000-000000000001",
"reference_number": "SUB-2026-000001",
"previous_status": "eingegangen",
"new_status": "zugeordnet",
"changed_at": "2026-01-01T12:00:00.000Z"
}
}support.threshold_reached
Payload fields
submission_id— stringreference_number— objectsupport_count— numberthreshold— numberreached_at— string
Example payload
{
"event_type": "support.threshold_reached",
"timestamp": "2026-01-01T12:00:00.000Z",
"municipality_id": "00000000-0000-0000-0000-0000000000aa",
"data": {
"submission_id": "00000000-0000-0000-0000-000000000001",
"reference_number": "SUB-2026-000001",
"support_count": 25,
"threshold": 25,
"reached_at": "2026-01-01T12:00:00.000Z"
}
}survey.response_received
Payload fields
survey_id— stringresponse_id— stringreceived_at— string
Example payload
{
"event_type": "survey.response_received",
"timestamp": "2026-01-01T12:00:00.000Z",
"municipality_id": "00000000-0000-0000-0000-0000000000aa",
"data": {
"survey_id": "00000000-0000-0000-0000-000000000002",
"response_id": "00000000-0000-0000-0000-000000000003",
"received_at": "2026-01-01T12:00:00.000Z"
}
}sla.escalation_triggered
Payload fields
submission_id— stringreference_number— objectsla_hours— numberescalated_at— string
Example payload
{
"event_type": "sla.escalation_triggered",
"timestamp": "2026-01-01T12:00:00.000Z",
"municipality_id": "00000000-0000-0000-0000-0000000000aa",
"data": {
"submission_id": "00000000-0000-0000-0000-000000000001",
"reference_number": "SUB-2026-000001",
"sla_hours": 48,
"escalated_at": "2026-01-01T12:00:00.000Z"
}
}Signature verification
Every request includes the header X-Signature: sha256=<hex>. The value is the HMAC-SHA256 of the raw request body (before JSON parsing) signed with the secret generated in the admin. Verify the signature server-side before processing the payload.
Verifying the signature with curl + openssl
# Recompute the signature locally and compare
SIGNATURE=$(echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
# expected header: X-Signature: sha256=<SIGNATURE>Verifying the signature in Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'
export function isValidSignature(rawBody: string, header: string | undefined, secret: string): boolean {
if (!header?.startsWith('sha256=')) return false
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
const provided = header.slice('sha256='.length)
try {
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'))
} catch {
return false
}
}Check signature
Compute the HMAC-SHA256 signature for a sample payload and your secret.
Delivery contract
decidio attempts each delivery up to four times (1 initial + 3 retries) with a backoff of 1 minute, 5 minutes, and 30 minutes. Each attempt has a 30-second timeout. Any HTTP 2xx status counts as "delivered"; other statuses or network errors trigger a retry. Every request carries the header User-Agent: decidio-webhooks/1.0.
Related topics
Dig deeper:
