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: