Skip to Content

Webhooks

When the first-party channels don’t fit, point WatchDeck at any HTTP(S) URL and it will POST (or PUT, or PATCH) a JSON payload on every incident state change. Use it for PagerDuty, OpsGenie, custom incident systems, internal Slack apps with stricter formatting needs, anything else.

Required fields

FieldNotes
Webhook URLAny http:// or https:// URL. Encrypted at rest.
MethodPOST (default), PUT, or PATCH.
HeadersOptional. Key/value rows merged on top of Content-Type and User-Agent defaults.
Body templateOptional. If empty, WatchDeck sends the default envelope. If set, your template string is rendered. Template length capped at 16,000 characters (~16 KB) on input.

The URL is encrypted at rest (AES-256-GCM); headers and body template are stored in plaintext. Leaving the URL empty on edit keeps the existing value.

Configure a webhook channel

Default envelope

If you don’t set a body template, WatchDeck sends Content-Type: application/json with this shape:

{ "event": "incident_opened", "severity": "critical", "title": "Endpoint down", "summary": "HTTP 502 — expected 200", "detail": "HTTP 502 — expected 200", "link": "https://app.watchdeck.dev/incidents/abc123", "endpoint": { "id": "ep_…", "name": "Production API", "url": "https://api.example.com/health" }, "incident": { "id": "inc_abc123", "startedAt": "2026-05-01T14:30:00.000Z", "status": "active" }, "fields": [ { "label": "Endpoint", "value": "Production API" }, { "label": "Target", "value": "https://api.example.com/health" }, { "label": "Cause", "value": "Endpoint down" } ], "tags": [], "actor": null, "idempotencyKey": "inc_abc123:incident_opened", "timestamp": "2026-05-01T14:30:01.234Z" }

event values

EventWhen
incident_openedIncident transitioned from nothing to active.
incident_resolvedIncident transitioned from active to resolved.
channel_testYou clicked Test on the channel card.

incident_escalated exists in the type system but no dispatcher writes it today — see Incidents → Escalation.

severity values

info · warning · critical · success. success is reserved for incident_resolved.

idempotencyKey

For incident dispatches it’s <incidentId>:<event> (so the same incident_opened for the same incident produces the same key on every retry). For test dispatches it’s test:<channelId>:<timestamp>. WatchDeck does not dedupe its own sends against the key — your receiver should use it to skip already-processed deliveries.

Custom body template

Templates use a small Handlebars-ish syntax. Available references mirror the default-envelope shape — {{event}}, {{severity}}, {{endpoint.name}}, {{incident.startedAt}}, etc.

{ "service": "{{endpoint.name}}", "severity": "{{severity}}", "summary": "{{summary}}", {{#if incident.id}}"dedup_key": "{{incident.id}}",{{/if}} "details": {{json fields}}, "fired_at": "{{isoNow}}" }

Helpers

HelperBehaviour
{{path.to.value}}Substitutes the value at that dotted path.
{{#if path}}…{{/if}}Conditionally includes the block if the path resolves truthy.
{{json key}}Emits the value at key as a JSON literal (object / array / string-with-quotes).
{{isoNow}}Current UTC time as an ISO 8601 string.

Content-Type negotiation

After rendering, WatchDeck tries to JSON.parse your output:

  • Parses → request goes out as Content-Type: application/json.
  • Doesn’t parse → request goes out as Content-Type: text/plain; charset=utf-8.

To force a different content type, set it in the Headers map — your value overrides the default.

Headers

Defaults applied to every request:

Content-Type: application/json (or text/plain — see above) User-Agent: WatchDeck-Webhook/1.0

Your custom headers are merged on top, so you can:

  • Override Content-Type for downstream APIs that demand a vendor-specific value.
  • Add Authorization — bearer tokens, basic auth, vendor-specific keys.
  • Add a vendor dedupe header if your downstream uses one (e.g. Idempotency-Key).
⚠️

Custom headers are stored in plaintext in the database. Treat anything you paste here the same way you would a CI environment variable — rotate them when collaborators leave.

Signing

⚠️

WatchDeck does not sign webhook payloads today. There is no X-WatchDeck-Signature header, no shared secret, no HMAC. If your receiver needs cryptographic authenticity, put a static bearer token in the Authorization header — it travels over TLS and is at least gateway-style auth.

HMAC payload signing is on the roadmap; this page will document it once it lands.

Test send

Click Test on the channel card to fire a channel_test payload at the URL. The result is shown inline on the card and recorded in the Delivery log with kind='channel_test' and idempotencyKey='test:<channelId>:<ts>'.

A successful test sets is_connected=true and updates last_success_at; a failed test does the opposite.

Send a test webhook payload

Failure handling

Receiver responseCaptured as
Any 2xxLogged as delivery_status='sent'. Body and request stored.
Any non-2xx (4xx + 5xx)failure_reason = HTTP <code> · <body>
Network / timeout (10s)failure_reason = the underlying error message

There’s no automatic retry on the cloud dispatcher today — the channel form’s Retry on failure toggle is stored but not yet wired. A failed delivery stays failed; the Test button is the fastest way to confirm a fix.

Common targets

  • PagerDuty Events API — POST to your service’s events.pagerduty.com/v2/enqueue URL with the integration key in Authorization or in a body template.
  • OpsGenie — POST to api.opsgenie.com/v1/json/genericwebhook with Authorization: GenieKey <key>.
  • Generic incident receivers — Statuspage, BetterStack incoming webhooks, Linear, Jira ServiceDesk, Notion incoming HTTP, Zapier hooks all accept the default envelope or a templated body.
  • Internal services — your own ops API. Include a bearer token in the headers; verify the idempotencyKey server-side.

Quotas and limits

  • Counts toward your per-plan channel cap — see Notifications → Quotas.
  • WatchDeck doesn’t apply its own per-channel rate limit. If your receiver needs a slower drip, gate it on its end.
  • Body template length: 16,000 characters (~16 KB), measured on the template string itself before substitution.

What’s next

Last updated on