diff --git a/apps/web/src/views/docs.ejs b/apps/web/src/views/docs.ejs index f1b67fe..5f22844 100644 --- a/apps/web/src/views/docs.ejs +++ b/apps/web/src/views/docs.ejs @@ -57,6 +57,9 @@ Account Monitors + Reliability + Notifications + Webhook payload Fields @@ -135,6 +138,11 @@ "request_body": "{\"ping\": true}", // optional — Content-Type defaults to application/json "regions": ["eu-central", "us-west"], // optional — default: all regions "timeout_ms": 10000, // optional — default: 10000 + "max_retries": 2, // optional — retry N times before declaring DOWN. Default: 0 + "retry_interval_s": 30, // optional — seconds between retries. Default: 30 + "resend_interval": 10, // optional — re-alert every Nth consecutive DOWN beat. 0 = never. Default: 0 + "cert_alert_days": 14, // optional — alert when TLS cert is within N days of expiry. 0 disables. Default: 14 + "channel_ids": ["<uuid>"], // optional — notification channels to attach "query": { ... } // optional — see Query Language below } @@ -149,6 +157,11 @@ request_bodystringRequest body (Content-Type defaults to application/json) regionsstring[]Regions to ping from: eu-central, us-west. Default: all timeout_msnumberRequest timeout in milliseconds (default: 10000) + max_retriesnumberRetry a failing check this many times before posting a DOWN result. Default: 0. Max: 10. See Reliability. + retry_interval_snumberSeconds between retries. Default: 30. + resend_intervalnumberIf a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0. + cert_alert_daysnumberFire a separate cert notification when the TLS certificate is within N days of expiring. 0 disables. Default: 14. + channel_idsstring[]Notification channel IDs to attach. See Notifications. queryobjectQuery conditions — see below @@ -170,13 +183,167 @@

Ping History

GET/monitors/:id/pings?limit=100
-

Returns recent ping results for a monitor. Max 1000.

+

Returns recent ping results for a monitor. Max 1000. Each ping carries an important boolean — true on status transitions and resend ticks (the beats that triggered notifications).

+ + + +
+

Reliability & alert noise

+

PingQL doesn't immediately fire on a single failed check. Three knobs let you tune how reactive vs. how stable the alerting is:

+ +

Retries before DOWN

+

If a check fails and max_retries is greater than zero, the runner waits retry_interval_s seconds and retries up to that many times before recording a DOWN result. A successful retry posts a single UP ping with meta.retries noting how many attempts it took. This kills almost all flapping caused by transient TCP resets, brief 5xx blips, or network jitter.

+ +

Important beats & transitions

+

Every check is recorded, but the important flag on a ping is only set when the monitor's state changes (UP↔DOWN) for that region. Notifications fire on important beats only — never on every routine check. State is tracked independently per region: if us-west goes DOWN, only a subsequent us-west UP clears it. eu-central being healthy will not silence a us-west outage.

+ +

Resend interval

+

For long outages, set resend_interval to re-fire the notification every Nth consecutive DOWN beat. With resend_interval: 10, a still-broken monitor produces an extra alert every 10 down checks. 0 (the default) means: alert once on the transition, then stay quiet until recovery.

+ +

Cert expiry alerting

+

For HTTPS monitors PingQL extracts the TLS leaf certificate's days-until-expiry on every check. When that drops at or below cert_alert_days for the first time, a separate cert notification fires (one per region). The flag clears when the cert is renewed, so each renewal cycle gets exactly one alert. Set cert_alert_days: 0 to disable.

+ +

Default empty query

+

If you don't supply a query, the monitor is considered up only on a 2xx response. Redirects (3xx), client errors (4xx) and server errors (5xx) all count as DOWN. Use the QL if you want different behaviour.

+
+ + +
+

Notifications

+

Notification channels are reusable destinations attached to monitors. When an important beat fires (DOWN, recovery, or cert), each attached channel is dispatched. PingQL ships with a webhook provider; more (Discord, Slack, Email) are designed as drop-ins.

+ +

List channels

+
GET/notifications/channels
+

Returns all channels for the authenticated account.

+ +

Create channel

+
POST/notifications/channels
+
+
json — request body
+
{
+  "name":    "On-call webhook",
+  "kind":    "webhook",
+  "config":  {
+    "url":     "https://hooks.example.com/pingql",
+    "headers": { "X-Team": "infra" },  // optional
+    "secret":  "shared-hmac-secret"          // optional — signs payloads
+  },
+  "enabled": true           // optional — default true
+}
+
+ + + + + + + + +
FieldTypeDescription
namestringDisplay name (up to 200 chars)
kindstringProvider type. Currently only webhook.
configobjectProvider-specific config. For webhook, requires url (http/https). Optional headers object and secret for HMAC signing.
enabledbooleanDisabled channels are skipped during dispatch but remain attached.
+ +

Update channel

+
PATCH/notifications/channels/:id
+

All fields optional. Provide a partial body to change name, config, or enabled state.

+ +

Delete channel

+
DELETE/notifications/channels/:id
+

Removes the channel and all monitor attachments to it.

+ +

Test channel

+
POST/notifications/channels/:id/test
+

Sends a synthetic test event through the channel and returns whether the provider accepted it. Useful to verify the URL and HMAC are wired correctly without waiting for a real outage.

+ +

Attaching channels to monitors

+

Pass channel_ids as an array of channel UUIDs when creating or patching a monitor. The PATCH replaces the full set; pass an empty array to detach all channels. Channels can only be attached to monitors in the same account.

+
+
http
+
PATCH /monitors/abc123def456
+Authorization: Bearer <key>
+Content-Type: application/json
+
+{ "channel_ids": ["5fb1c0bf-…", "a72e0d91-…"] }
+
+
+ + +
+

Webhook payload

+

Webhook channels POST a JSON body to the configured URL on every event. The HTTP method is POST, the request times out after 5 seconds, and PingQL does not retry — the next important beat is the retry. Failures are logged but never block ingest.

+ +

Headers

+ + + + + + + + +
HeaderDescription
content-typeapplication/json
user-agentPingQL-Notifier/1
x-pingql-signatureHex-encoded HMAC-SHA256 of the raw request body, keyed by config.secret. Only present when a secret is configured. Verify it server-side to confirm the request came from PingQL.
customAny headers from config.headers are forwarded as-is.
+ +

Body shape

+

Every payload has the same envelope:

+
+
json
+
{
+  "channel": { "id": "<uuid>", "name": "On-call webhook" },
+  "event": { "kind": "down" | "up" | "cert" | "test", ... }
+}
+
+ +

Event types

+

down — fired on the first DOWN important beat for a region, and again every resend_intervalth consecutive down if configured.

+
json — event
+
{
+  "kind": "down",
+  "monitor": {
+    "id":     "abc123def456",
+    "name":   "My API",
+    "url":    "https://api.example.com/health",
+    "region": "us-west"     // "" for unspecified/single-region monitors
+  },
+  "ping": {
+    "status_code": 503,
+    "latency_ms":  412,
+    "error":       null,
+    "checked_at":  "2026-04-08T14:23:00.000Z"
+  }
+}
+ +

up — fired on recovery, only when that same region transitions back from DOWN. Same shape as down.

+ +

cert — fired once per renewal cycle when the TLS leaf cert drops at or below cert_alert_days for a region.

+
json — event
+
{
+  "kind": "cert",
+  "monitor": { "id": "…", "name": "…", "url": "…", "region": "us-west" },
+  "days": 9           // days until certificate expires
+}
+ +

test — synthetic event from POST /notifications/channels/:id/test. The monitor object is a placeholder.

+
json — event
+
{
+  "kind": "test",
+  "monitor": { "id": "test", "name": "Test event", "url": "https://example.com", "region": "" }
+}
+ +

Verifying the signature

+
+
node
+
import { createHmac, timingSafeEqual } from "crypto";
+
+function verify(rawBody, headerSig, secret) {
+  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
+  return timingSafeEqual(Buffer.from(expected), Buffer.from(headerSig));
+}
+
+

Always verify against the raw request body before parsing JSON.

Query Language — Fields

-

A PingQL query is a JSON object evaluated against each ping. If it returns true, the monitor is up. Default (no query): up when status < 400.

+

A PingQL query is a JSON object evaluated against each ping. If it returns true, the monitor is up. Default (no query): up only on a 2xx status. Redirects and errors all count as DOWN.

FieldTypeDescription