Skip to content

Webhooks ​

Send every alert to your own endpoint as JSON. A webhook is a notification channel like Slack or e-mail, so anything you can route to a channel you can route to a webhook: incidents, advisory warnings, DNS changes, certificate renewals and account-security events.

Not the reseller webhooks

This page is the customer-facing contract. Hoppla also has a reseller webhook system with its own catalogue — team.*, member.*, email.* suppression — described in Reseller Webhooks. Those events are only delivered to reseller plans, and the reseller API is likewise reseller-only. Everything on THIS page works on any paid plan.

Set one up ​

  1. Settings → Notification Channels → Add Channel, type Webhook.
  2. Paste the URL that should receive the POST.
  3. Optionally set an HMAC Secret. Do set one — without it you cannot prove a request came from Hoppla.
  4. Send Test delivers a real request immediately, so you can confirm the endpoint before an incident depends on it.
  5. Point a notification rule at the channel to choose which events it receives.

Your URL must be publicly resolvable over HTTP(S). Private, link-local and loopback addresses are rejected when the channel is saved and again at delivery — Hoppla will not call localhost, 10.0.0.0/8, 169.254.0.0/16 or friends, which also means a tunnel is needed for local testing.

The request ​

POST, Content-Type: application/json, 10-second timeout.

Only a 2xx counts as delivered. Anything else — a 4xx, a 5xx, or a redirect, which we do not follow — is a failure, and so is a timeout or a connection error.

What happens next depends on the code. A 5xx, 429, 408 or 425, or a connection failure, is treated as a bad moment on your side and retried: three attempts at 10s, 60s and 300s. Any other 4xx says the request itself was wrong — a 404 for a URL that moved, a 422 for a payload your handler rejected — and is not retried, because the next two attempts would be identical. Either way the delivery is recorded, with the status your endpoint returned, under Notifications → Logs. Each row there has a Send again button: it replays that delivery with the same X-Webhook-Delivery id, so if your endpoint already handled the first attempt it will recognise the repeat and drop it. Pressing it when you are not sure whether something landed is safe.

Every request carries an X-Webhook-Delivery header, repeated as delivery_id in the body. It identifies one delivery and does not change between retries, so it is the key to deduplicate on. See Delivering more than once.

The User-Agent identifies the sending platform, for example Hoppla-Webhook/1.0. On a white-label portal it carries that portal's brand instead.

The body is a flat JSON object — no nested monitor or incident objects — with keys sorted alphabetically. Every payload carries event and timestamp; the rest depends on the event.

json
{
  "check_type": "uptime",
  "delivery_id": "018f3c2a-7b41-7c3e-9a55-2f9b1d4e6a10",
  "event": "incident.created",
  "incident_id": "9f1c…",
  "monitor_id": "3ab7…",
  "monitor_name": "example.com",
  "monitor_url": "https://example.com",
  "severity": "critical",
  "started_at": "2026-03-04T12:00:00+00:00",
  "status": "down",
  "timestamp": "2026-03-04T12:00:03+00:00",
  "title": "HTTP 503 Service Unavailable"
}

Fields are drawn from a fixed allowlist, so a payload never carries anything from your account that the event did not need: incident_id, monitor_id, monitor_name, monitor_url, title, status, severity, started_at, resolved_at, check_type, event_type, timestamp, zone, nameservers, changes, nameserver_change, nameservers_changed, ssl_issuer, ssl_valid_from, ssl_valid_to, response_time_ms, status_code, cleared_warning, brand_name. security.* events carry their own curated payload and never include credential values — only the name of the thing that changed.

Verifying the signature ​

When the channel has a secret, Hoppla signs the request:

X-Webhook-Signature: <hex>

Until 28 September 2026 deliveries also carried the same value in X-Hoppla-Signature (and X-Hoppla-Timestamp). That pair has been removed. A receiver that still reads it only needs to read X-Webhook-Signature (and X-Webhook-Timestamp) instead; the value is identical.

The signature is HMAC-SHA256 of the raw request body, hex-encoded:

signature = hmac_sha256(secret, raw_request_body)

Sign the bytes you received, not a re-serialised object. Hoppla sends the JSON with forward slashes unescaped and keys sorted; parsing and re-encoding will change the bytes and the signature will not match.

php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $secret);

if (! hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
js
import { createHmac, timingSafeEqual } from 'node:crypto'

// express.raw({ type: 'application/json' }) — req.body must be a Buffer
const expected = createHmac('sha256', secret).update(req.body).digest('hex')
const got = req.get('x-webhook-signature') ?? ''

const ok = expected.length === got.length &&
  timingSafeEqual(Buffer.from(expected), Buffer.from(got))
if (!ok) return res.sendStatus(401)
python
import hmac, hashlib

expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Webhook-Signature", "")):
    abort(401)

Compare in constant time (hash_equals, timingSafeEqual, compare_digest), not with ==.

Events ​

Every event below can be routed to a webhook channel. The same list is shown in the product under Developers, generated from the same source as this page.

certificate ​

EventMeaning
certificate.renewedA TLS certificate was replaced with a new one.

dns ​

EventMeaning
dns.records_changedA watched DNS record changed value.

incident ​

EventMeaning
incident.createdAn incident was opened for a monitor.
incident.resolvedAn open incident was resolved and the all-clear sent.

monitor ​

EventMeaning
monitor.flappingA monitor changed state repeatedly in a short window.
monitor.flapping_resolvedA flapping monitor settled.

security ​

EventMeaning
security.mfa_enabledA member turned on multi-factor authentication.
security.mfa_disabledA member turned off multi-factor authentication.
security.recovery_codes_regeneratedA member regenerated their recovery codes, invalidating the old set.
security.mfa_resetAn administrator reset a member’s multi-factor authentication.
security.passkey_addedA member registered a passkey.
security.passkey_removedA passkey was removed from an account.
security.password_changedA member changed their password.
security.email_changedA member’s e-mail address was changed.
security.api_token_createdAn API token was issued.
security.api_token_revokedAn API token was revoked.
security.session_revokedA session was signed out remotely.
security.login_new_deviceA sign-in came from a device this account has not used before.
security.account_blockedAn account was blocked and can no longer sign in.
security.account_unblockedA blocked account was restored.
security.role_changedA member’s role changed, altering what they may do.
security.auth_policy_changedThe team’s authentication policy changed (SSO, MFA enforcement).
security.account_deletedAn account was deleted. For GDPR erasure see member.erased.

storm ​

EventMeaning
storm.detectedMany monitors failed at once — usually shared infrastructure, not many faults.
storm.resolvedA detected storm has passed.

warning ​

EventMeaning
warning.ssl_expiringA TLS certificate is approaching expiry.
warning.domain_expiringA domain registration is approaching expiry.
warning.degraded_performanceResponse times are consistently worse than the monitor’s threshold.
warning.slow_responseA single check exceeded the slow-response threshold.
warning.application_healthAn application health endpoint reported a problem.
warning.sitemap_issuesThe sitemap is unreachable, malformed, or contains dead URLs.
warning.security_headersExpected security headers are missing or weakened.
warning.redirect_chainA redirect chain grew too long, loops, or drops to plain HTTP.
warning.robots_txtrobots.txt changed, disappeared, or now blocks crawling.
warning.dnsbl_listingThe host appeared on a DNS blocklist.
warning.broken_linksA crawl found links that no longer resolve.
warning.mixed_contentAn HTTPS page loads sub-resources over plain HTTP.
warning.server_resourcesAn agent monitor reported CPU, memory or disk pressure.
warning.clearedA previously reported warning is no longer present.

Delivering more than once ​

Delivery is at-least-once. A slow endpoint that eventually succeeds may already have processed the first attempt, so the same event can legitimately arrive twice.

Deduplicate on X-Webhook-Delivery (also delivery_id in the body). It is one value per delivery and it is identical on every retry of that delivery, so recording the ones you have handled and skipping repeats is enough:

php
$id = $request->header('X-Webhook-Delivery');

if (! Cache::add("webhook:{$id}", true, now()->addDay())) {
    return response()->noContent();   // already handled
}

A retry reuses the id; a genuinely new event gets a new one. Don't key on incident_id plus event — a monitor that recovers and fails again produces two real incident.created events you want both of.

Things worth knowing ​

  • Order is not guaranteed. Two events raised in the same second may arrive in either order. Use timestamp rather than arrival order.
  • Answer fast. The timeout is 10 seconds; queue the work and return 2xx immediately rather than processing inline.
  • A secret is optional but a missing one is unverifiable. Without it the request carries no signature at all, and anyone who learns your URL can post to it.

Limits ​

Every delivery is a POST with Content-Type: application/json. There is no way to change the method or add headers of your own, by design — the request is authenticated by its HMAC signature, not by something you paste into a header.

Don't put a token in the URL either. The channel's URL is shown in full to everyone on your team who can read the channel, so it is not a place to keep a secret. Use the signature above.