---
title: 'Webhook'
description: 'Send every incident lifecycle event to your own HTTP endpoint as JSON, schema-versioned for forward compatibility.'
section: 'Integrations'
canonical_url: 'https://yorkermonitoring.com/docs/integrations/webhook'
---

# Webhook

The webhook integration posts a JSON body for every incident lifecycle event to your own HTTP endpoint. Use this for custom integrations, Opsgenie, Zapier, workflow engines, or anywhere Yorker doesn't ship a purpose-built adapter.

For the underlying model (lifecycle states, event types, scoped hypothesis), see [Incidents](/docs/concepts/incidents).

## Set up

```bash
curl -X POST https://yorkermonitoring.com/api/notification-channels \
  -H "Authorization: Bearer $YORKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "incident-sink",
    "channel": {
      "type": "webhook",
      "url": "https://hooks.example.com/yorker-incidents",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer ${INCOMING_TOKEN}"
      }
    }
  }'
```

| Field      | Required | Default | Description                                  |
| ---------- | -------- | ------- | -------------------------------------------- |
| `url`      | yes      | n/a     | Destination endpoint                         |
| `method`   | no       | `POST`  | `POST` or `PUT`                              |
| `headers`  | no       | n/a     | Extra headers (e.g., auth). A `Content-Type` header (any casing) is rejected at create/update time. |

Yorker always sends `Content-Type: application/json`. A user-supplied `Content-Type` header would break the documented body-parser contract and is refused by the channel schema.

## What gets sent

The webhook channel receives **every** incident event by default:

- `opened`
- `alert_attached`
- `severity_changed`
- `acknowledged`
- `auto_resolved`
- `closed`
- `reopened`
- `note_added`

### Default payload

```json
{
  "schema_version": 1,
  "event": {
    "eventId": "ievt_001",
    "incidentId": "inc_abc",
    "teamId": "team_123",
    "eventType": "opened",
    "actor": { "type": "system", "id": null },
    "payload": {
      "eventType": "opened",
      "observations": {
        "sources": ["synthetic_http"],
        "syntheticHttp": {
          "affectedChecks": [{ "checkId": "chk_api", "checkName": "Checkout API" }],
          "symptomWindow": { "startedAt": "2026-04-15T09:58:00.000Z" },
          "errorSignature": {
            "httpStatusCodes": [503, 504],
            "errorCategories": ["upstream_error"],
            "locationsAffected": ["loc_us_east_1", "loc_eu_west_1"],
            "sampleMessages": ["Bad Gateway", "Gateway Timeout"]
          },
          "sharedFailingDomains": ["api.stripe.com"]
        }
      },
      "hypothesis": {
        "summary": "Stripe API is returning 503/504; checkout is blocked.",
        "confidence": 0.75,
        "ruledIn": ["shared_failing_domain=api.stripe.com"],
        "ruledOut": ["DNS resolution: NXDOMAIN not observed", "TLS: handshake completes"],
        "correlationDimensionsMatched": ["shared_failing_domain", "error_pattern"],
        "scope": "external_symptoms_only"
      },
      "title": "Checkout API outage",
      "severity": "critical",
      "fingerprintHash": "…",
      "memberAlertInstanceIds": ["ainst_1", "ainst_2"],
      "recurrenceOf": []
    },
    "occurredAt": "2026-04-15T10:00:00.000Z"
  },
  "incident": {
    "incidentId": "inc_abc",
    "title": "Checkout API outage",
    "severity": "critical",
    "state": "open",
    "openedAt": "2026-04-15T10:00:00.000Z",
    "triageUrl": "https://yorkermonitoring.com/dashboard/incidents/inc_abc"
  }
}
```

### schema_version

Every default payload carries `schema_version: 1`. Gate your consumer on this field and Yorker will not silently break your integration when the default shape evolves: breaking changes bump the version; additive changes don't.

## Observations shape

Each source in `observations.sources[]` (snake_case: `synthetic_http`, `synthetic_browser`, `synthetic_mcp`) has a matching camelCase block (`syntheticHttp`, `syntheticBrowser`, `syntheticMcp`) on the same object. A multi-source incident carries every relevant block. Example consumer:

```ts
const obs = event.payload.observations;
if (obs.sources.includes("synthetic_http")) {
  // obs.syntheticHttp is present
  const statusCodes = obs.syntheticHttp.errorSignature.httpStatusCodes;
}
```

## Template overrides

Render your own JSON body with Handlebars. The rendered string must parse as valid JSON; on failure, the default payload is sent instead.

### Edit in the web UI

Open **Settings > Notification Channels** and click **Templates** next to the webhook channel. The editor has JSON syntax highlighting, a live preview that renders your body against one of six canonical fixtures, and a **library** sidebar with two starter bodies (**Default: flat envelope** and **Nested: incident + event objects**) plus curated examples (PagerDuty Events API v2-shaped, OTel log record shape) you can apply with a click.

**Send test** posts the current saved template to your webhook URL with the selected fixture's context (60-second cooldown per channel). The audit row for the test run is written to `incident_notification_dispatches` with `detail_json.isTest = true` so a real incident replay can be distinguished from a smoke-test.

### Edit via the API

```bash
curl -X PUT https://yorkermonitoring.com/api/notification-channels/nch_abc \
  -H "Authorization: Bearer $YORKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "incidentTemplate": {
      "channelType": "webhook",
      "overrides": {
        "opened": {
          "body": "{\"type\":\"incident.opened\",\"id\":\"{{incident.incidentId}}\",\"severity\":\"{{incident.severity}}\",\"hypothesis\":\"{{payload.hypothesis.summary}}\",\"triage\":\"{{incident.triageUrl}}\"}"
        }
      }
    }
  }'
```

For payloads that want to splat in arbitrary nested structure without mustache-ing every key, use `{{jsonBody payload}}`.

The render context has the following top-level keys (same as the [Slack integration](/docs/integrations/slack)). The event envelope fields mirror `serializeIncidentEventForExport`: `eventId`, `eventType`, `incidentId`, `teamId`, `actor`, `occurredAt`, `payload`. In addition, a materialized `incident` snapshot (`title`, `severity`, `state`, `openedAt`, `triageUrl`) is exposed for direct use in templates. There is no top-level `event` key; use the individual fields or helper as shown below.

```json
{
  "body": "{\"type\":\"{{eventType}}\",\"id\":\"{{eventId}}\",\"occurredAt\":\"{{occurredAt}}\",\"actor\":{{{jsonBody actor}}},\"payload\":{{{jsonBody payload}}},\"incident\":{{{jsonBody incident}}} }"
}
```

Notes:

- JSON-producing templates (webhook, Slack, PagerDuty, ServiceNow) compile with Handlebars HTML escaping disabled, so for these channels `{{foo}}` and `{{{foo}}}` produce identical output. Triple-stash is shown here by convention: it makes the intent (raw interpolation into JSON) obvious to readers. Email HTML templates compile with escaping on, where the two forms are NOT equivalent: `{{jsonBody payload}}` gets HTML-escaped by default (safe) and `{{{jsonBody payload}}}` is an explicit opt-out of escaping that the template author must choose deliberately.
- Handlebars' tokenizer fails on a mustache close that runs directly into a JSON `}`. Both `{{{foo}}}}` (triple-close + literal) and `{{foo}}}` (double-close + literal) raise a parse error. Add a space before the JSON close brace (`{{{foo}}} }`) to disambiguate. The rendered JSON is otherwise unchanged; if your consumer verifies a canonical-JSON HMAC over the body, re-serialize (e.g., `JSON.stringify(JSON.parse(body))`) before hashing so whitespace differences don't break the signature.

A render error or invalid-JSON result falls back to the default payload and logs a warning. Dispatch never fails on a bad template.

Helpers and render context are the same as the [Slack integration](/docs/integrations/slack).

## Delivery and retry

- **Timeout:** Yorker expects a response within the platform HTTP timeout. Slow endpoints risk being recorded as `failed`.
- **Retry:** Yorker does not retry failed webhook deliveries on the same event. Use the [audit trail](/docs/concepts/incidents#audit-trail) (`incident_notification_dispatches`) to replay deliveries from your own backfill tooling.
- **Dedupe:** Within a 30-second window, a duplicate event to the same channel is recorded as `skipped_dedupe` and not re-sent. This protects against runner retry bursts.

## Disabling

Set `incidentSubscribed: false` to fall back to the legacy per-alert webhook dispatch.
