<%~ include('./partials/public-head', { title: 'Documentation', nav: 'docs' }) %>

Overview

PingQL is a developer-friendly uptime monitoring API. Monitors are defined with a URL, an interval, and an optional query that determines what "up" means for your service.

Base URL: https://api.pingql.com

Authentication

All API requests require an account key passed as a Bearer token:

http
Authorization: Bearer <your-64-char-hex-key>

Create an account at /dashboard or via the API. Keys are 64-character hex strings (256-bit). Shown once at registration — store them securely.

Account

Register

POST/account/register

Create a new account. Email is optional and used only for account recovery.

json — request body
{ "email": "you@example.com" }  // optional
json — response
{ "key": "5bf5311b56d09254c8a1f0e3...", "email_registered": true }

Update Email

POST/account/email

Set or update the recovery email for an existing account.

json — request body
{ "email": "you@example.com" }

Monitors

List

GET/monitors/

Returns all monitors for the authenticated account.

Create

POST/monitors/
json — request body
{
  "name":       "My API",
  "url":        "https://api.example.com/health",
  "interval_s": 60,        // check every 60 seconds (min: 2)
  "method":     "POST",    // optional — default: GET
  "request_headers": { "X-Api-Key": "secret" },  // optional
  "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
}
FieldTypeDescription
namestringDisplay name for the monitor
urlstringURL to monitor
interval_snumberCheck interval in seconds (min: 30 free, 2 pro)
methodstringHTTP method — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
request_headersobjectCustom headers as key-value pairs
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

Get

GET/monitors/:id

Returns a monitor including its most recent ping results.

Update

PATCH/monitors/:id

Update any field. All fields are optional.

Delete

DELETE/monitors/:id

Toggle

POST/monitors/:id/toggle

Enable or disable a monitor without deleting it.

Ping History

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

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 only on a 2xx status. Redirects and errors all count as DOWN.

FieldTypeDescription
statusnumberHTTP status code
bodystringFull response body as text
headers.namestringResponse header, e.g. headers.content-type
$responseTimenumberRequest latency in milliseconds
$certExpirynumberDays until SSL certificate expires
$jsonobjectJSONPath expression against response body
$selectobjectCSS selector against response HTML

Query Language — Operators

OperatorDescriptionTypes
$eqEqual toany
$neNot equal toany
$gt / $gteGreater than / or equalnumber
$lt / $lteLess than / or equalnumber
$containsString contains substringstring
$startsWithString starts withstring
$endsWithString ends withstring
$regexMatches regular expressionstring
$existsField is present and non-nullany
$inValue is in arrayany
json
// simple equality shorthand
{ "status": 200 }

// operator form
{ "status": { "$lt": 400 } }
{ "body": { "$contains": "healthy" } }
{ "headers.content-type": { "$contains": "application/json" } }

$json — JSONPath

Extract and compare a value from a JSON response body. The key is a dot-notation path starting with $.

json
// response body: { "status": "ok", "db": { "connections": 12 } }

{ "$json": { "$.status": { "$eq": "ok" } } }
{ "$json": { "$.db.connections": { "$lt": 100 } } }

$select — CSS Selector

Extract text content from an HTML response using a CSS selector. Useful for monitoring public pages without an API.

json
// matches if <h1> text is exactly "Example Domain"
{ "$select": { "h1": { "$eq": "Example Domain" } } }

// matches if status badge contains "operational"
{ "$select": { ".status-badge": { "$contains": "operational" } } }

Logical Operators

json
// $and — all conditions must match
{ "$and": [{ "status": 200 }, { "body": { "$contains": "ok" } }] }

// $or — any condition must match
{ "$or": [{ "status": 200 }, { "status": 204 }] }

// $not — invert a condition
{ "$not": { "status": 500 } }

$consider

By default, matching conditions mean the monitor is up. Set "$consider": "down" to flip this. If the conditions match, the monitor is down.

json
// down if response time exceeds 2 seconds
{ "$consider": "down", "$responseTime": { "$gt": 2000 } }

// down if cert expires in less than 7 days
{ "$consider": "down", "$certExpiry": { "$lt": 7 } }

// down if any of these match
{
  "$consider": "down",
  "$or": [
    { "status": { "$gte": 500 } },
    { "$responseTime": { "$gt": 5000 } }
  ]
}

Examples

Basic health endpoint

json
{ "status": 200, "body": { "$contains": "healthy" } }

JSON API response shape

json
{
  "$and": [
    { "status": 200 },
    { "$json": { "$.ok": { "$eq": true } } }
  ]
}

Performance monitor (mark down if slow)

json
{ "$consider": "down", "$responseTime": { "$gt": 1000 } }

Cert expiry alert

json
{ "$consider": "down", "$certExpiry": { "$lt": 14 } }

Status page (HTML)

json
{ "$select": { ".status-indicator": { "$eq": "All systems operational" } } }
<%~ include('./partials/public-foot') %>