Uptimeify Docs
Integrations

Webhook payload reference

The exact JSON Uptimeify POSTs to a generic webhook channel, the request headers it sends, and how to verify the HMAC signature.

A Webhook notification channel delivers every alert, recovery, reminder and warning for a customer to an HTTP endpoint you control. This page documents the payload of the generic webhook channel: the JSON body, the request headers, and signature verification.

Chat and on-call channels (Slack, Discord, Microsoft Teams, PagerDuty, Opsgenie, …) send a payload in that provider's own format instead. The body documented here is what a Webhook channel sends when no custom body template is configured.

The request

PropertyValue
MethodPOST (configurable per channel)
Content typeapplication/json
Timeout30 seconds (configurable per channel)

An endpoint that repeatedly fails to accept a connection gets a shorter timeout per attempt. Delivery is not stopped by this; only the cost of a failing attempt goes down.

Request headers

HeaderMeaning
User-AgentIdentifies the Uptimeify monitoring fleet
X-Webhook-TimestampTime the request was built, ISO 8601 (UTC)
X-Webhook-AttemptAttempt counter for this notification, starting at 1
X-Webhook-SignatureHMAC signature of the body, only when a secret is configured
X-Webhook-Signature-Algorithmsha256, sent alongside the signature

Custom headers configured on the channel are merged in and win over the defaults.

Verifying the signature

When you configure a secret on the channel, we sign the exact bytes of the request body with HMAC-SHA256 and send the result hex-encoded in X-Webhook-Signature. Verify against the raw body, before any JSON parsing and re-serialisation, otherwise whitespace differences will break the comparison.

import crypto from 'node:crypto'

function isValidSignature(rawBody, headerValue, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(expected, 'utf8')
  const b = Buffer.from(String(headerValue ?? ''), 'utf8')
  // Length check first: timingSafeEqual throws on differing lengths.
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Payload

{
  "type": "alert",
  "check_degraded": false,
  "incident_id": 8094,
  "message": "Website Acme Shop is DOWN",
  "expected_response_error": null,
  "ssl": null,
  "website": {
    "id": 10597,
    "name": "Acme Shop",
    "url": "https://shop.example.com",
    "custom_fields": {}
  },
  "customer": {
    "id": 42,
    "name": "Acme GmbH",
    "email": "ops@example.com",
    "custom_fields": {}
  },
  "incident": {
    "type": "http_status",
    "started_at": "19.08.2026, 17:32:04",
    "resolved_at": "N/A",
    "status": "open",
    "ssl": null,
    "error_details": {
      "check_degraded": false,
      "status_code": 503,
      "error_message": "Unexpected status code (503) (expected: 200)",
      "expected_response_error": null,
      "response_time_ms": 412,
      "blocking_response": {
        "status_code": 503,
        "vendor": "Cloudflare",
        "headers": {
          "server": "cloudflare",
          "cf-ray": "a2daec8259183caa-CDG"
        }
      }
    },
    "locations": ["de-fra", "fi-hel", "pl-waw"],
    "screenshot": null,
    "screenshot_url": "https://…",
    "diagnostics": null
  }
}

Top-level fields

FieldTypeNotes
typestringalert, recovery, reminder, warning, unstable or slow
check_degradedbooleantrue when the check could not reach a verdict, as opposed to a confirmed outage
incident_idnumberStable per incident, the same across alert, reminder and recovery
messagestringOne human-readable line, ready to post
expected_response_errorstring | nullConvenience copy of incident.error_details.expected_response_error
sslobject | nullCertificate details, set only for SSL incidents
websiteobjectid, name, url, custom_fields
customerobjectid, name, email, custom_fields
incidentobjectSee below

incident

FieldTypeNotes
typestringSee incident types below
started_atstringDD.MM.YYYY, HH:MM:SS in Europe/Berlin, or "N/A"
resolved_atstringSame format, "N/A" while the incident is open
statusstringIncident status as stored, for example open or resolved
sslobject | nullSame object as the top-level ssl
error_detailsobjectSee below
locationsstring[]The monitoring locations this notification covers
screenshotstring | nullBase64 JPEG, only when a screenshot was captured
screenshot_urlstring | nullLink to the same screenshot
diagnosticsobject | nullRaw diagnostics of the failing check; contents vary by failure

incident.error_details

FieldTypeNotes
check_degradedbooleanSame value as the top-level field
status_codenumber | nullHTTP status; 0 means no response was received at all
error_messagestring | nullThe failure as recorded
expected_response_errorstring | nullSet when an "Expected Response" rule failed
response_time_msnumber | nullResponse time of the failing check
blocking_responseobject | nullWho rejected the request, see below

incident.type

http_status, downtime (delivered as down), response_time, https_redirect, keyword_check, page_size, ssl_handshake, ssl_expiry, ssl_warning, domain_expiry, domain_expiry_warning, dns.

For a service monitor, downtime is delivered as the protocol instead: icmp, smtp, ssh, ftp or imap-pop.

blocking_response: who rejected the request

A 403, 406, 429 or 503 says that something refused the request. It does not say whether your site is down or whether a web application firewall sorted our check out before it reached your server. When the response carries headers that identify the sender, we pass them through in this field. It is null for every other status, and also when the response named nobody.

FieldTypeNotes
status_codenumberThe status that triggered the capture
vendorstring | nullNamed provider, for example Cloudflare, Sucuri, Imperva, DataDome, Akamai, Amazon CloudFront, Fastly, Bunny, Varnish, Google. null when no header names one; we do not guess
headersobjectHeader name to value, lowercase names

The captured headers are a fixed allowlist: server, via, retry-after, x-cache, x-served-by, cf-ray, cf-cache-status, cf-mitigated, x-sucuri-id, x-sucuri-block, x-iinfo, x-cdn, x-akamai-transformed, akamai-grn, x-amz-cf-id, x-amz-cf-pop, x-datadome, x-request-id, x-waf-event-id, cdn-pullzone, cdn-requestid. Cookies and authentication headers are never captured and never sent, and each value is truncated at 200 characters.

Use it to tell two situations apart that look identical in the status code alone:

const blocked = payload.incident?.error_details?.blocking_response
if (blocked) {
  // Our check was rejected by an edge, the origin may be perfectly healthy.
  notifyOps(`${blocked.vendor ?? 'An edge'} rejected the check with ${blocked.status_code}`)
}

Delivery conditions and body templates

A webhook channel can carry conditions, in which case a notification is only delivered when the payload matches them, and a custom JSON body template, in which case your template replaces the body documented above. Both are configured on the channel in the app.

On this page