---
title: "Webhook Payload Reference — Uptrack"
description: "Exact JSON payloads Uptrack sends your webhook when a monitor goes down or recovers, signed with HMAC-SHA256. Verify and parse them in Node, Python or Elixir."
url: https://uptrack.app/guides/webhook-payloads
---

# Webhook payload reference

When an incident happens, Uptrack sends a POST request to your webhook URL with a JSON payload. This page documents every field you will receive for each event type.

## Event types

Uptrack sends two types of webhook events:

| Event        | When it fires                                        |
| ------------ | ---------------------------------------------------- |
| monitor.down | A monitor fails its check and an incident is created |
| monitor.up   | A monitor recovers and the incident is resolved      |

## `monitor.down`

Sent when a monitor fails its health check and a new incident is created.

POST https://your-server.com/webhooks/uptrack

```
{
  "event": "monitor.down",
  "timestamp": "2026-04-01T08:23:14Z",
  "monitor": {
    "id": "0192f1c4-7c8a-7000-8000-4b1d2e3f5a6c",
    "name": "Production API",
    "url": "https://api.example.com/health",
    "check_interval": 60,
    "protocol": "https"
  },
  "incident": {
    "id": "0192f1d0-3e21-7000-8000-9c8b7a6d5e4f",
    "started_at": "2026-04-01T08:23:14Z",
    "cause": "HTTP 503 Service Unavailable"
  },
  "check": null
}
```

### Field descriptions

| Field                   | Type           | Description                                                                                                                                                 |
| ----------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| event                   | string         | Always "monitor.down" for this event type                                                                                                                   |
| timestamp               | string         | ISO 8601 timestamp of when the event was emitted                                                                                                            |
| monitor.id              | string         | Unique monitor identifier                                                                                                                                   |
| monitor.name            | string         | Human-readable monitor name                                                                                                                                 |
| monitor.url             | string         | The URL being monitored                                                                                                                                     |
| monitor.check\_interval | integer        | Check frequency in seconds (e.g. 30)                                                                                                                        |
| monitor.protocol        | string \| null | "https", "http" or "tcp"; null for other monitor types                                                                                                      |
| incident.id             | string         | Unique incident identifier                                                                                                                                  |
| incident.started\_at    | string         | ISO 8601 timestamp of when the incident started                                                                                                             |
| incident.cause          | string         | Human-readable reason for the failure                                                                                                                       |
| check                   | object \| null | Details of the failing check (status code, response time, location). null for now; the field is always present, so you can rely on it when it is filled in. |

## `monitor.up`

Sent when a monitor recovers and the open incident is resolved.

POST https://your-server.com/webhooks/uptrack

```
{
  "event": "monitor.up",
  "timestamp": "2026-04-01T08:31:47Z",
  "monitor": {
    "id": "0192f1c4-7c8a-7000-8000-4b1d2e3f5a6c",
    "name": "Production API",
    "url": "https://api.example.com/health",
    "check_interval": 60,
    "protocol": "https"
  },
  "incident": {
    "id": "0192f1d0-3e21-7000-8000-9c8b7a6d5e4f",
    "started_at": "2026-04-01T08:23:14Z",
    "resolved_at": "2026-04-01T08:31:47Z",
    "duration_seconds": 513
  },
  "check": null
}
```

### Additional fields

| Field                      | Type    | Description                                          |
| -------------------------- | ------- | ---------------------------------------------------- |
| incident.resolved\_at      | string  | ISO 8601 timestamp of when the incident was resolved |
| incident.duration\_seconds | integer | Total downtime duration in seconds                   |

The `monitor` and `check` objects have the same shape as `monitor.down`. The `incident.cause` field is omitted on recovery events.

## Request headers

Every webhook request includes these headers:

```
Content-Type: application/json
User-Agent: Uptrack-Webhook/1.0
X-Uptrack-Event: monitor.down
X-Uptrack-Signature: t=1712000000,v1=7ae11423d0db7008f5a3a3195bc742a615cb29ddbddd1d096e3f52056a3ab17b
```

| Field               | Type   | Description                                                                        |
| ------------------- | ------ | ---------------------------------------------------------------------------------- |
| Content-Type        | string | Always "application/json"                                                          |
| User-Agent          | string | Always "Uptrack-Webhook/1.0"                                                       |
| X-Uptrack-Event     | string | The event type ("monitor.down" or "monitor.up")                                    |
| X-Uptrack-Signature | string | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your signing secret> |

## Verifying webhook requests

Each webhook channel has its own signing secret. It starts with `whsec_`, and Uptrack shows it once, when you create the channel. Every request carries`X-Uptrack-Signature: t=…,v1=…`.`v1` is the hex HMAC-SHA256 of the timestamp, a dot and the raw request body.

Compute the same value with your secret, compare in constant time, and reject requests older than 5 minutes. Use the raw body bytes, before any JSON parsing.

verify.js (Node)

```
const crypto = require('crypto')

function verifyUptrack(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries((header || '').split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1 || '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
```

verify.py (Python)

```
import hashlib, hmac, time

def verify_uptrack(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split('=', 1) for p in (header or '').split(',') if '=' in p)
    t = int(parts.get('t', 0))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get('v1', ''))
```

Lost the secret? Create the channel again, or rotate it with the API:`PATCH /api/alert-channels/{id}`with `{"config": {"rotate_secret": true}}`. The reply carries the new secret once, and the old one stops working at once.

## Code examples

Receive and parse Uptrack webhook payloads in your language of choice.

### Node.js (Express)

server.js

```
const express = require('express')
const app = express()

// Keep the raw body: the signature covers the exact bytes Uptrack sent.
app.post('/webhooks/uptrack', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('X-Uptrack-Signature')
  if (!verifyUptrack(req.body, signature, process.env.UPTRACK_WEBHOOK_SECRET)) {
    return res.status(401).send('Bad signature')
  }
  const { event, monitor, incident } = JSON.parse(req.body)

  switch (event) {
    case 'monitor.down':
      console.log(`[DOWN] ${monitor.name} — ${incident.cause}`)
      // Page your on-call team, create a ticket, etc.
      break

    case 'monitor.up':
      console.log(`[UP] ${monitor.name} — was down for ${incident.duration_seconds}s`)
      // Resolve the ticket, notify the channel
      break

  }

  res.status(200).send('OK')
})

app.listen(3000)
```

### Python (Flask)

app.py

```
import json, os
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/uptrack', methods=['POST'])
def uptrack_webhook():
    raw = request.get_data()
    if not verify_uptrack(raw, request.headers.get('X-Uptrack-Signature', ''), os.environ['UPTRACK_WEBHOOK_SECRET']):
        return jsonify(error='bad signature'), 401
    data = json.loads(raw)
    event = data['event']
    monitor = data['monitor']

    if event == 'monitor.down':
        incident = data['incident']
        print(f"[DOWN] {monitor['name']} — {incident['cause']}")

    elif event == 'monitor.up':
        incident = data['incident']
        print(f"[UP] {monitor['name']} — down for {incident['duration_seconds']}s")

    return jsonify(status='ok'), 200
```

### Elixir (Phoenix)

webhook\_controller.ex

```
defmodule MyAppWeb.WebhookController do
  use MyAppWeb, :controller

  def uptrack(conn, %{"event" => "monitor.down"} = params) do
    monitor = params["monitor"]
    incident = params["incident"]
    IO.puts("[DOWN] #{monitor["name"]} — #{incident["cause"]}")

    json(conn, %{status: "ok"})
  end

  def uptrack(conn, %{"event" => "monitor.up"} = params) do
    monitor = params["monitor"]
    incident = params["incident"]
    IO.puts("[UP] #{monitor["name"]} — down for #{incident["duration_seconds"]}s")

    json(conn, %{status: "ok"})
  end
end
```

## Testing webhooks locally

Use one of these tools to inspect webhook payloads before writing any code:

### webhook.site

Go to `webhook.site` to get a unique URL. Paste it as your webhook URL in Uptrack, then trigger a test alert. You will see the exact headers and payload in your browser.

### ngrok

Run `ngrok http 3000` to expose your local server. Use the generated URL as your webhook endpoint in Uptrack. Requests hit your local code so you can debug with breakpoints.

## Integration ideas

### Custom Slack formatting

Parse the webhook payload and post a rich Slack message with Block Kit. Include the monitor name, downtime duration, and a direct link to your status page.

### PagerDuty trigger

Forward `monitor.down` events to PagerDuty's Events API v2 to page your on-call engineer. Auto-resolve when `monitor.up` arrives.

### Zapier / Make

Use a Zapier "Catch Hook" trigger to receive Uptrack webhooks, then route them to Google Sheets, Jira, Linear, or any of 5,000+ apps without writing code.

### Incident log database

Store every webhook payload in a database table. Query historical incidents, calculate MTTR, or build your own uptime reporting dashboard.

## Start receiving webhook alerts

Pro includes 50 monitors at 30-second checks. Start free, or choose a plan for more.

[See plans](https://uptrack.app/signup)

## Related reading

- [Connect Uptrack to Zapier](https://uptrack.app/guides/zapier)
- [Slack slash commands](https://uptrack.app/guides/slack-commands)
- [n8n node](https://uptrack.app/integrations/n8n)
- [Uptime monitoring](https://uptrack.app/features/uptime-monitoring)
