> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uptimeio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Send monitor alerts to any HTTP endpoint, with signed payloads you can verify

Webhook destinations send monitor alerts as JSON to any HTTP endpoint, so you can drive custom systems, automation platforms and internal tools. Webhooks are available on every plan, including Free.

Every webhook is signed with a secret unique to that endpoint, so you can verify that a request genuinely came from UptimeIO.

## Setting Up a Webhook

<Steps>
  <Step title="Open Destinations">
    Go to **Destinations** in the sidebar, click **Add Destination**, then choose **Webhook**.
  </Step>

  <Step title="Configure the Webhook">
    | Field | Required | Description |
    | - | - | - |
    | **Name** | Yes | A friendly name to identify this webhook |
    | **Webhook URL** | Yes | `https://` (or `http://`) endpoint to receive notifications |
    | **HTTP Method** | No | `POST` or `PUT` (default `POST`) |
    | **Headers** | No | Custom HTTP headers as JSON. Up to 50 headers; names up to 100 characters, values up to 1,000. |

    The URL must be publicly reachable. `localhost` and private IP ranges are rejected. Use HTTPS in production.
  </Step>

  <Step title="Configure Events">
    Select which events trigger this webhook. All six are enabled by default; at least one must stay enabled.
  </Step>

  <Step title="Copy your signing secret">
    Open the integration and reveal **Signing secret** (`whsec_…`). Store it in your
    receiving application — you need it to verify incoming requests.
  </Step>

  <Step title="Test">
    Use the **Test** button to verify connectivity.
  </Step>
</Steps>

## Webhook Payload

Every UptimeIO webhook has the same body shape, regardless of which event fired:

```json theme={null}
{
  "version": "1",
  "event": "monitor.down",
  "timestamp": 1757203200000,
  "monitor": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Production API",
    "url": "https://api.example.com/health",
    "type": "HTTP"
  },
  "incident": {
    "id": "880e8400-e29b-41d4-a716-446655440000",
    "started_at": 1757203200000,
    "resolved_at": null
  },
  "subject": "Production API is down",
  "message": "Connection timeout after 10000ms"
}
```

| Field | Type | Description |
| - | - | - |
| `version` | string | Envelope version. Currently always `"1"`. |
| `event` | string | Event type. See the table below. |
| `timestamp` | number | Epoch **milliseconds**, not an ISO string. |
| `monitor` | object | `id`, `name`, `url`, `type`. Always present. |
| `incident` | object \| null | `null` for events that are not incident-scoped. |
| `incident.started_at` | number \| null | Epoch ms the incident began. `null` when the emitting path does not carry it (see note below). |
| `incident.resolved_at` | number \| null | Epoch ms the incident was resolved, or `null` while still open. |
| `subject` | string | Short human-readable summary. |
| `message` | string | Longer human-readable detail. |

<Info>
  `incident.started_at` can be `null` on recovery events. UptimeIO sends `null`
  rather than substituting the resolution time, so that a `null` is never mistaken
  for an incident that began the instant it ended. Treat `started_at` as optional.
</Info>

### Event types

| Event | Fired when |
| - | - |
| `monitor.down` | A monitor is confirmed failing |
| `monitor.up` | A monitor recovers |
| `monitor.ssl_expiring` | A TLS certificate is nearing expiry |
| `monitor.domain_expiring` | A domain registration is nearing expiry |
| `monitor.slow_response` | Response time crossed the slow-response threshold |
| `monitor.slow_response_resolved` | Response time returned to normal |

<Info>
  New event types may be added in future. Ignore events you do not recognise
  rather than rejecting the request.
</Info>

## Headers

Every request includes:

| Header | Description |
| - | - |
| `Content-Type` | Always `application/json` |
| `User-Agent` | `UptimeIO-Webhook/1.0` |
| `X-UptimeIO-Event` | The event type, matching `event` in the body |
| `X-UptimeIO-Delivery` | Unique UUID for this delivery attempt |
| `X-UptimeIO-Timestamp` | Epoch ms, matching `timestamp` in the body |
| `X-UptimeIO-Signature` | `sha256=<hex>` — see below |

## Verifying the Signature

The signature is an HMAC-SHA256 over the string `{timestamp}.{raw request body}`,
keyed with your endpoint's signing secret, hex-encoded and prefixed with `sha256=`.

<Warning>
  Compute the HMAC over the **raw request body bytes**, exactly as received. If you
  parse the JSON and re-serialize it before hashing, the signature will not match.
</Warning>

```javascript theme={null}
const crypto = require('crypto');

// express: capture the raw body
app.use('/webhook/uptimeio', express.raw({ type: 'application/json' }));

app.post('/webhook/uptimeio', (req, res) => {
  const rawBody = req.body.toString('utf8');
  const timestamp = req.get('X-UptimeIO-Timestamp');
  const received = req.get('X-UptimeIO-Signature');

  const expected =
    'sha256=' +
    crypto
      .createHmac('sha256', process.env.UPTIMEIO_WEBHOOK_SECRET)
      .update(`${timestamp}.${rawBody}`, 'utf8')
      .digest('hex');

  // Constant-time comparison avoids leaking the secret via timing.
  const valid =
    received &&
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!valid) {
    return res.status(401).json({ error: 'invalid signature' });
  }

  const payload = JSON.parse(rawBody);
  console.log('Alert:', payload.subject);

  res.status(200).json({ received: true });
});
```

<Tip>
  Also reject requests whose `X-UptimeIO-Timestamp` is far from your current clock
  (a few minutes' tolerance is typical). This limits replay of a captured payload.
</Tip>

### Rotating the secret

Open the destination and choose **Rotate secret** (API: `POST /api/integrations/{instanceId}/rotate-secret`, session token). The previous secret stops working immediately, so have your receiving application ready to switch: verification fails between rotation and deployment.

## Custom Headers

Add authentication or custom metadata:

```json theme={null}
{
  "Authorization": "Bearer your-secret-token",
  "X-Custom-Header": "custom-value"
}
```

<Info>
  Custom headers cannot override `Content-Type` or any `X-UptimeIO-*` header;
  those are set by UptimeIO and take precedence.
</Info>

## Troubleshooting

| Issue | Solution |
| - | - |
| Not receiving requests | Verify the endpoint is publicly accessible and uses HTTPS. Private IPs and `localhost` are rejected at save time. |
| Signature never matches | You are almost certainly hashing a re-serialized body. Hash the raw bytes, and confirm you are using `{timestamp}.{body}`, not the body alone. |
| Signature header missing | The endpoint has no signing secret yet. Open the integration to generate one, then re-test. |
| 4xx from your endpoint | Any 4xx response is treated as a configuration problem and is not retried. Check the URL and any authentication headers. |
| 5xx from your endpoint | Check your server logs. 5xx responses are treated as temporary failures. |
| Timeouts | Ensure the endpoint responds within 10 seconds |
| Delivery result | Open the incident and check **Notification delivery** |

## Next Steps

<CardGroup cols={2}>
  <Card title="Notification Setup" icon="bell" href="/notifications/overview">
    Assign webhooks directly to monitors
  </Card>

  <Card title="Events" icon="filter" href="/notifications/overview#events">
    Choose which events trigger notifications
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.