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:
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
Create a new account. Email is optional and used only for account recovery.
{ "email": "you@example.com" } // optional
{ "key": "5bf5311b56d09254c8a1f0e3...", "email_registered": true }
Update Email
Set or update the recovery email for an existing account.
{ "email": "you@example.com" }
Monitors
List
Returns all monitors for the authenticated account.
Create
{
"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
}
| Field | Type | Description |
|---|---|---|
| name | string | Display name for the monitor |
| url | string | URL to monitor |
| interval_s | number | Check interval in seconds (min: 30 free, 2 pro) |
| method | string | HTTP method — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
| request_headers | object | Custom headers as key-value pairs |
| request_body | string | Request body (Content-Type defaults to application/json) |
| regions | string[] | Regions to ping from: eu-central, us-west. Default: all |
| timeout_ms | number | Request timeout in milliseconds (default: 10000) |
| max_retries | number | Retry a failing check this many times before posting a DOWN result. Default: 0. Max: 10. See Reliability. |
| retry_interval_s | number | Seconds between retries. Default: 30. |
| resend_interval | number | If a monitor stays DOWN, re-fire a notification every Nth consecutive down beat. 0 disables resend. Default: 0. |
| cert_alert_days | number | Fire a separate cert notification when the TLS certificate is within N days of expiring. 0 disables. Default: 14. |
| channel_ids | string[] | Notification channel IDs to attach. See Notifications. |
| query | object | Query conditions — see below |
Get
Returns a monitor including its most recent ping results.
Update
Update any field. All fields are optional.
Delete
Toggle
Enable or disable a monitor without deleting it.
Ping History
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
Returns all channels for the authenticated account.
Create channel
{
"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
}
| Field | Type | Description |
|---|---|---|
| name | string | Display name (up to 200 chars) |
| kind | string | Provider type. Currently only webhook. |
| config | object | Provider-specific config. For webhook, requires url (http/https). Optional headers object and secret for HMAC signing. |
| enabled | boolean | Disabled channels are skipped during dispatch but remain attached. |
Update channel
All fields optional. Provide a partial body to change name, config, or enabled state.
Delete channel
Removes the channel and all monitor attachments to it.
Test channel
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.
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
| Header | Description |
|---|---|
| content-type | application/json |
| user-agent | PingQL-Notifier/1 |
| x-pingql-signature | Hex-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. |
| custom | Any headers from config.headers are forwarded as-is. |
Body shape
Every payload has the same envelope:
{
"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.
{
"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.
{
"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.
{
"kind": "test",
"monitor": { "id": "test", "name": "Test event", "url": "https://example.com", "region": "" }
}Verifying the signature
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.
| Field | Type | Description |
|---|---|---|
| status | number | HTTP status code |
| body | string | Full response body as text |
| headers.name | string | Response header, e.g. headers.content-type |
| $responseTime | number | Request latency in milliseconds |
| $certExpiry | number | Days until SSL certificate expires |
| $json | object | JSONPath expression against response body |
| $select | object | CSS selector against response HTML |
Query Language — Operators
| Operator | Description | Types |
|---|---|---|
| $eq | Equal to | any |
| $ne | Not equal to | any |
| $gt / $gte | Greater than / or equal | number |
| $lt / $lte | Less than / or equal | number |
| $contains | String contains substring | string |
| $startsWith | String starts with | string |
| $endsWith | String ends with | string |
| $regex | Matches regular expression | string |
| $exists | Field is present and non-null | any |
| $in | Value is in array | any |
// 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 $.
// 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.
// 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
// $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.
// 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
{ "status": 200, "body": { "$contains": "healthy" } }JSON API response shape
{
"$and": [
{ "status": 200 },
{ "$json": { "$.ok": { "$eq": true } } }
]
}Performance monitor (mark down if slow)
{ "$consider": "down", "$responseTime": { "$gt": 1000 } }Cert expiry alert
{ "$consider": "down", "$certExpiry": { "$lt": 14 } }Status page (HTML)
{ "$select": { ".status-indicator": { "$eq": "All systems operational" } } }