Uptime checksHTTP, keyword and TLS checks on your addresses, with the evidence of every failure kept.
Cron heartbeatsOne curl line at the end of a job. Silence past the grace window is the alert.
Status pagesA page your customers read, on your own domain, with the incident in plain words.
ExplanationsA written reason with the alert: what failed, what the checks saw, what to look at first.
on every plan Alerts to six channelsImportTeams
APIRead, pause, acknowledge and post updates from your own scripts.
MCP serverAsk your AI assistant what is down, then acknowledge or post an update.
WebhooksSigned JSON when an incident opens, is acknowledged or recovers.
Heartbeat guidescrontab, GitHub Actions, Railway, Node, Go, WordPress and Kubernetes.
DocsShort guides, each one task, with the lines to copy.
GuidesWhy monitors send false alarms, why cron jobs stop, and the fix.
ChangelogThe big things we shipped, newest first.
Trust and securityWhere your data lives, who processes it, how long we keep it.
Referral program soonRecommend Upglory to another team. In the works.
Contact
Docs menu Developers / Webhooks

Alerts as signed JSON, to your own address.

A webhook channel posts one JSON event to your https address each time an incident opens, repeats, is acknowledged or recovers. Every request is signed with a secret only you and we hold.

6 minute read Reference

Adding a webhook

In Alerts, press Add a channel, choose Webhook and paste your address. It has to be https on port 443 or a port from 1024 to 65535, on a public address, with no user name or password in it. We send a test event at once and show the signing secret. The secret is shown this once; copy it into your receiver before you close the dialog.

Events

  • incident.opened A monitor or a heartbeat went down and an incident opened. For a heartbeat, source.kind is heartbeat.
  • incident.repeat The incident is still open and nobody has acknowledged it, sent on the workspace's repeat rule.
  • incident.acknowledged Somebody acknowledged the incident. The body says when, never who.
  • incident.recovered The incident closed. Sent when the workspace sends recovery alerts, which is the default.
  • test Sent when the webhook is added and on every Send test. It carries the workspace only.

An incident.opened request, as it arrives:

POST /hooks/catch/1234/abcd
Content-Type: application/json
User-Agent: Upglory-Webhook/1
Upglory-Event-Id: evt_af5287e260ccc5b7dd86af3408
Upglory-Timestamp: 1792494120
Upglory-Signature: v1=5f2a9c...e81b
{
  "version": 1,
  "event": "incident.opened",
  "event_id": "evt_af5287e260ccc5b7dd86af3408",
  "sent_at": "2026-10-20T11:02:00Z",
  "workspace": {
    "id": "0199f3a2-7c41-7d1e-9a52-2f6b8c1d4e90",
    "name": "Example Shop"
  },
  "source": {
    "kind": "monitor",
    "id": "0199f3a3-4d5e-7f60-8a1b-2c3d4e5f6a7b",
    "name": "checkout",
    "url": "https://example.shop/checkout"
  },
  "incident": {
    "id": "0199f3a6-1b2c-7e3d-8f4a-5b6c7d8e9f01",
    "number": 42,
    "state": "open",
    "opened_at": "2026-10-20T11:01:30Z",
    "cause": "expected 200, got 502 from Frankfurt and Stockholm",
    "regions": [
      "Frankfurt",
      "Stockholm"
    ],
    "acknowledged": false,
    "link": "https://upglory.io/app/incidents/0199f3a6-1b2c-7e3d-8f4a-5b6c7d8e9f01"
  }
}

The other incident events carry the same body with their own event, event_id and state. A test carries the workspace only:

{
  "version": 1,
  "event": "test",
  "event_id": "evt_6c1f0e9b2d4a8f3c7e5b1a90d2",
  "sent_at": "2026-10-20T11:02:00Z",
  "workspace": {
    "id": "0199f3a2-7c41-7d1e-9a52-2f6b8c1d4e90",
    "name": "Example Shop"
  }
}

Payload reference

FieldWhat it holds
versionAlways 1 for this body. A change that would break a receiver gets a new version.
eventOne of the events above.
event_idThe delivery's id, the same as the Upglory-Event-Id header.
sent_atWhen the delivery was made, ISO 8601 in UTC.
workspace.id, workspace.nameThe workspace the alert is from.
source.kindmonitor or heartbeat.
source.id, source.nameWhat the incident is about.
source.urlA monitor's address. Left out for a heartbeat, and for a monitor marked as carrying secrets in its address.
incident.id, incident.numberThe incident, and its number in the workspace.
incident.stateopen or resolved.
incident.opened_at, incident.resolved_atWhen it opened, and when it closed once it has.
incident.causeWhat failed, in words, with the locations that saw it.
incident.regionsThe locations that saw it fail, by city.
incident.acknowledged, incident.acknowledged_atWhether somebody acknowledged it, and when.
incident.linkThe incident in the dashboard.

No body carries a person's name or email address, and none carries a check's headers or response body.

Headers

HeaderValueWhat it is for
Content-Typeapplication/jsonThe body is one JSON object.
User-AgentUpglory-Webhook/1The 1 is the body's version.
Upglory-Event-Idevt_...The same on every try of one delivery. Keep the ids you have handled and drop a repeat.
Upglory-Timestamp1760958120When the delivery was made, in Unix seconds. The signature covers it.
Upglory-Signaturev1=5f2a9c...The hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the signing secret. While a replaced secret is still valid there are two values, comma separated, the new secret's first.

Verify a signature

Check every request before you act on it:

  1. Read the body exactly as it arrived, before any JSON parsing. A body parsed and written out again is not the one we signed.
  2. Refuse a timestamp more than five minutes away from your clock, so an old request sent again is turned away.
  3. Compute the HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your signing secret, as hex.
  4. Compare it with each v1= value in constant time, and accept the request when one matches.

Node

import crypto from 'node:crypto';

// rawBody is the request body as it arrived, a Buffer, before any JSON parsing.
export function verifyUpglory(rawBody, headers, secret) {
  const timestamp = headers['upglory-timestamp'] ?? '';
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!/^\d+$/.test(timestamp) || age > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  return (headers['upglory-signature'] ?? '').split(',').some((part) => {
    if (!part.startsWith('v1=')) return false;
    const given = Buffer.from(part.slice(3), 'hex');
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

Python

import hashlib
import hmac
import time


def verify_upglory(raw_body: bytes, headers, secret: str) -> bool:
    """raw_body is the request body as it arrived, before any JSON parsing."""
    timestamp = headers.get("Upglory-Timestamp", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    for part in headers.get("Upglory-Signature", "").split(","):
        if part.startswith("v1=") and hmac.compare_digest(part[3:], expected):
            return True
    return False

Go

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// VerifyUpglory checks a webhook request. rawBody is the body as it
// arrived, read before any JSON decoding.
func VerifyUpglory(rawBody []byte, h http.Header, secret string) bool {
	ts := h.Get("Upglory-Timestamp")
	sent, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || time.Since(time.Unix(sent, 0)).Abs() > 5*time.Minute {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(rawBody)
	expected := mac.Sum(nil)
	for _, part := range strings.Split(h.Get("Upglory-Signature"), ",") {
		hexSig, ok := strings.CutPrefix(part, "v1=")
		given, err := hex.DecodeString(hexSig)
		if ok && err == nil && hmac.Equal(given, expected) {
			return true
		}
	}
	return false
}

Replacing the secret

In the channel's menu, Replace the secret makes a new one and shows it once. The old one keeps working for 24 hours: during that day every request carries two values in Upglory-Signature, the new secret's first, so a receiver that checks either keeps working while you switch.

Retries and duplicates

Answer with any 2xx within ten seconds to take a delivery. A network error, a 5xx, a 408 or a 429 is tried again, up to three tries in all, with a growing wait; a Retry-After is honoured up to five minutes. Any other 4xx is final and is not tried again. Redirects are never followed: a 3xx is a failed delivery, so give us the final address.

Delivery is at least once, so the same delivery can arrive twice, for example when your answer is lost on its way back. Every try of one delivery carries the same Upglory-Event-Id; keep the ids you have handled and drop a repeat. When a delivery fails, the channel shows why on its row and its page, and your inbox says so once.

Limits

  • Webhooks per workspace10
  • Deliveries per webhook in an hour60
  • Time to answer10 seconds
  • Tries per delivery3
  • Test sends per workspace in an hour10
  • Replaced secret keeps working24 hours

Past the hourly figure, deliveries to that webhook are dropped for the rest of the hour and listed as dropped on its page; your inbox says so once, and every other channel still gets them.

Other guides