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.
{
"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.
{
"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 carriesX-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.
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)
}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)
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)
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'), 200Elixir (Phoenix)
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
endTesting 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