# Webhooks

A webhook contact posts a JSON document to an HTTPS endpoint of yours each
time it is alerted: your own service, or an automation tool that accepts
webhooks. Attach it to monitors like any other contact.

## Setting one up

1. Add a contact of kind **Webhook** with the endpoint's URL, in the web
   app or with the API:

   ```json
   {
     "name": "Ops webhook",
     "kind": "webhook",
     "secret": { "url": "https://hooks.example.com/justmonitorit?token=7f3c..." }
   }
   ```

   The URL must be `https://` on a domain name (not an IP address). Put a
   token of your choosing in its path or query and check it on your side:
   it is how your endpoint tells our requests from anyone else's. The URL
   is stored encrypted and never shown again.

2. Prove that you control the domain. The new contact is `unverified`, and
   its `config` holds a DNS record to publish:

   ```json
   "config": {
     "host": "hooks.example.com",
     "txt_name": "_justmonitorit.hooks.example.com",
     "txt_value": "justmonitorit-webhook-verification=0123456789abcdef0123456789abcdef"
   }
   ```

   Add a TXT record with that name and value at your DNS provider. The
   value is your organisation's and is the same for every webhook, so one
   record covers every endpoint on that host. The record goes on the exact
   host of the URL; a record on a parent domain does not count.

3. Press **Check again** on the contact, or call
   `POST /orgs/{org}/contacts/{id}/verify/send`. Once the record is found
   the contact is `active`. Until then it is sent nothing, and the check
   answers `422` with `webhook_domain_unverified` and the record it looked
   for. DNS changes can take a few minutes to appear.

If the host already has your record (another webhook on it), a new webhook
is `active` at once. Changing the URL to another host checks that host.

With Terraform, the contact's `webhook_txt_name` and `webhook_txt_value`
attributes let your DNS provider's resource publish the record
([Terraform and Pulumi](terraform.md)).

## What we send

`POST` to your URL with `Content-Type: application/json`,
`User-Agent: JustMonitorIt-Webhook/<version>` and `X-JMI-Delivery: <delivery
id>`. One request carries every alert that is due for the contact at that
moment (alerts within a few seconds of each other are grouped):

```json
{
  "version": 1,
  "type": "alert",
  "delivery_id": "dlv_01j9...",
  "sent_at": "2026-10-06T14:07:14Z",
  "org": { "id": "org_01j9...", "name": "Acme" },
  "contact": { "id": "con_01j9...", "name": "Ops webhook" },
  "events": [
    {
      "id": "ntf_01j9...",
      "event": "down",
      "state": "DOWN",
      "at": "2026-10-06T14:07:13Z",
      "summary": "DOWN: API: Connection timed out after 10 s",
      "monitor": {
        "id": "mon_01j9...",
        "name": "API",
        "kind": "http",
        "target": "https://api.example.com/health",
        "url": "https://dash.justmonitorit.com/monitors/mon_01j9...?org=org_01j9..."
      },
      "incident": {
        "id": "inc_01j9...",
        "opened_at": "2026-10-06T14:03:01Z",
        "url": "https://dash.justmonitorit.com/incidents/inc_01j9...?org=org_01j9..."
      },
      "status": "down",
      "previous_status": "up",
      "error": { "kind": "connect_timeout", "summary": "Connection timed out after 10 s", "status_code": null },
      "degraded": null,
      "locations": {
        "failing": [{ "code": "lon1", "name": "London" }, { "code": "fra1", "name": "Frankfurt" }],
        "total": 3
      },
      "downtime_s": null,
      "reminder_n": null,
      "flap_transitions": null
    }
  ]
}
```

- `type` is `alert`, or `test` for **Send test alert** (with a `message`
  and no events).
- `event` is `down`, `degraded` (slow, or a certificate or domain close to
  expiry; see `degraded.reason`), `recovered`, `flapping` or `reminder`
  (still down). `state` is the short word for it: `DOWN`, `SLOW`, `CERT`,
  `DOMAIN`, `UP` or `FLAPPING`.
- `downtime_s` is set for `recovered` and `reminder`, `reminder_n` for
  `reminder`, `flap_transitions` for `flapping`. `degraded` holds
  `reason`, `latency_ms`, `degraded_after_ms`, `tls_days_left` and
  `domain_days_left`.
- New fields may be added to version 1; ignore the ones you do not know.

## Your response

Answer with any `2xx` status, within 15 seconds. We do not follow
redirects.

- `429`, `5xx`, a timeout or a refused connection: we retry with backoff
  from 10 seconds up to 30 minutes (a `Retry-After` header is honoured),
  for up to 6 hours or 24 attempts. A retry sends the same events again,
  so use each event's `id` to drop ones you already have; `delivery_id`
  changes on each attempt.
- `410 Gone`: the contact is disabled. **Check again** turns it back on.
- Other `4xx`, or a redirect: that alert is not retried. Five such failures
  in a row disable the contact, and the organisation's owners and admins
  get an email.

The contact page lists every delivery with its status code and the start of
your response.
