Skip to content

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 — string
  • reference_number — object
  • topic_category_name — object
  • topic_source — string
  • priority_level — string
  • created_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 — string
  • reference_number — object
  • previous_status — string
  • new_status — string
  • reason — object
  • changed_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 — string
  • reference_number — object
  • support_count — number
  • threshold — number
  • reached_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 — string
  • response_id — string
  • received_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 — string
  • reference_number — object
  • sla_hours — number
  • escalated_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.

Dig deeper: