Durabull Documentation

Webhook Notifications

Route Durabull alert events to HTTPS webhook endpoints with signed JSON payloads.

Webhook notifications let alert rules POST structured JSON to any HTTPS endpoint you control. Use them for custom paging, automation (Zapier, n8n), internal incident systems, or middleware that forwards alerts into Slack or PagerDuty.

Setup

You can configure webhooks in two ways:

  • Saved webhook destinations: reusable organization-level endpoints managed from settings.
  • Custom webhook URLs: one-off endpoints configured directly on an alert rule.

For reusable destinations:

  1. Open Settings → Alert destinations.
  2. Choose Add destination → Webhook and enter a name and endpoint URL.
  3. Optionally add a signing secret (minimum 16 characters). Self-hosted instances need DURABULL_SECRET_ENCRYPTION_KEY to store it; see Environment Variables.
  4. Use Test to verify delivery.
  5. In the connection's alert rule builder, select it under Saved destinations.

For a one-off endpoint, choose Webhook in the rule builder and enter its URL and optional signing secret. Routes can include email and Linear destinations too.

Saved destinations are resolved at dispatch time: edits affect pending deliveries and retries as well as future alerts. Legacy webhook references can retain enqueue-time URL and secret snapshots; migrate them to the current saved-destination picker to use the latest settings.

Payload format

Durabull sends versioned JSON with schemaVersion: 1.

{
  "schemaVersion": 1,
  "event": "alert.fired",
  "id": "alert-event-uuid",
  "deliveryId": "delivery-uuid",
  "occurredAt": "2026-05-25T12:00:00.000Z",
  "organization": { "id": "org_123", "slug": "acme" },
  "connection": { "id": "conn_123", "name": "Production Redis" },
  "rule": { "id": "rule_123", "name": "High failure rate", "type": "failure_rate" },
  "queue": { "name": "email-send" },
  "alert": {
    "status": "firing",
    "summary": "Failure rate exceeded threshold",
    "context": { "jobId": "42", "failedReason": "SMTP timeout" },
    "firedAt": "2026-05-25T12:00:00.000Z",
    "dedupeKey": null
  },
  "links": {
    "dashboard": "https://app.durabull.io/acme/c/conn_123/queues/email-send",
    "job": "https://app.durabull.io/acme/c/conn_123/queues/email-send/jobs/42",
    "muteRule": "https://app.durabull.io/acme/c/conn_123/alerts?ruleId=rule_123"
  }
}

Test deliveries use event: "alert.test" with synthetic context.

Durabull does not include full BullMQ job.data payloads in webhook bodies.

Request headers

HeaderDescription
Content-Typeapplication/json
User-AgentDurabull-Alerts/1.0
Idempotency-KeyStable alert event id — dedupe retries per notification destination
X-Durabull-Delivery-IdOutbox delivery id, reused across retries
X-Durabull-TimestampUnix seconds (when signing is enabled)
X-Durabull-Signaturesha256=<hex> HMAC (when signing is enabled)

Verifying signatures

When a signing secret is configured, verify:

signature = HMAC-SHA256(secret, "{timestamp}.{rawBody}")

Reject timestamps more than five minutes in the past or future to limit replay attacks. Verify the exact raw request body before parsing JSON; reserializing JSON changes the signature. Deduplicate accepted deliveries as well, because a valid request can be replayed within the window.

import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyDurabullWebhook(rawBody: string, headers: Headers, secret: string): boolean {
  const timestamp = headers.get('x-durabull-timestamp')
  const signature = headers.get('x-durabull-signature')
  if (!timestamp || !signature) return false
  if (!/^\d+$/.test(timestamp) || !/^sha256=[0-9a-f]{64}$/.test(signature)) return false
  const timestampSeconds = Number(timestamp)
  if (!Number.isSafeInteger(timestampSeconds)) return false
  if (Math.abs(Date.now() / 1000 - timestampSeconds) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  const provided = Buffer.from(signature.slice('sha256='.length), 'hex')
  const expectedBytes = Buffer.from(expected, 'hex')
  return provided.length === expectedBytes.length && timingSafeEqual(provided, expectedBytes)
}

Delivery behavior

  • Durabull uses at-least-once delivery with exponential backoff retries.
  • Respond with any 2xx status to acknowledge receipt.
  • 408, 429, and 5xx responses are retried.
  • Most other 4xx responses are treated as permanent failures.
  • Requests time out after 10 seconds.
  • Due retry attempts are swept by the existing alert monitor, so retries can continue even after the original alert is no longer actively firing.
  • Retry attempts stop after 7 days from the first delivery enqueue. The delivery row is marked permanently failed so dead endpoints do not retry forever.

Security notes

  • Production webhook URLs must use HTTPS.
  • Durabull blocks private and localhost targets (SSRF protection).
  • Saved destination signing secrets are encrypted at rest and masked in API responses.
  • Custom per-rule webhook signing secrets remain scoped to that alert rule and are masked in API responses after save.

HTTP is allowed when NODE_ENV=development or DURABULL_WEBHOOK_ALLOW_HTTP=true. Keep the override unset in production. Allowing HTTP does not permit private or localhost targets; use a publicly reachable test endpoint when validating local webhook delivery.