PingWise API
Everything the panel does with monitors, incidents, groups and status pages, over plain JSON. One key per script, the same validation and the same prices as the panel, and an OpenAPI 3.1 document you can generate a client from.
Getting started
Base URL: https://app.pingwise.com/api/v1. Make a key in Team → API keys (owners only), copy it once, and send it as a bearer token:
curl https://app.pingwise.com/api/v1/monitors \ -H "Authorization: Bearer pw_…"
Add a monitor the same way the panel’s New monitor form would, with the same rules and the same messages when something is off:
curl https://app.pingwise.com/api/v1/monitors \
-H "Authorization: Bearer pw_…" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "interval": 60}'
The answer is the new monitor, with its id, its state (pending until the first round comes back) and credits_per_month, what it costs at that interval.
Authentication
Every request carries Authorization: Bearer pw_…. A key belongs to one account and is either read only or read and write; pick the narrowest one that does the job, one key per integration, and revoke it from the same page when the integration goes. Keys are stored hashed; the panel shows the prefix only, and the full key exactly once.
- No key, or an unknown or revoked one:
401 unauthorized. - A read-only key on a write:
403 forbidden. A frozen account (deletion requested):403 account_frozen. - No sessions, no cookies, no CSRF: the key is the whole story. Use HTTPS; plain HTTP is not served.
Conventions
- JSON in and out. Send
Content-Type: application/jsonwith a JSON object as the body. Unknown fields are refused, so a typo can’t pass silently. - Times are ISO 8601 in UTC with a
Z, e.g.2026-10-31T08:18:44Z. Durations are seconds. - Ids: monitors, channels and accounts have UUIDs; incidents, groups and status pages have integers.
- Lists come as
{"data": […], "next_cursor": "…"}. Passnext_cursorback as?cursor=until it isnull.?limit=is 1–100, 50 by default. Monitors list oldest first; incidents newest first. - PATCH changes only the fields you send. Omitted fields stay;
nullclears a field that allows it. - Secrets never come back.
secret_headersandbasic_passwordare write only; responses carryhas_secret_headersandhas_basic_passwordinstead. Alert channels are listed without their addresses or tokens. - Deleting answers
204with no body.
Errors
Every error is a JSON object with an error code and usually a detail sentence. Validation errors add fields, a map from field name to a list of messages, the same messages the panel shows.
HTTP/1.1 422 Unprocessable Content
{"error": "invalid", "fields": {"url": ["Enter a valid URL."], "colour": ["Unknown field."]}}
| Status | error | When |
|---|---|---|
| 400 | bad_request | The body is not a JSON object. |
| 401 | unauthorized | No key, or an unknown or revoked one. |
| 403 | forbidden, account_frozen | A read-only key on a write; a hosting-panel-managed monitor the key may not change; the account is being deleted. |
| 404 | not_found | Not in this account. Reseller endpoints also answer 404 when the account is not a reseller. |
| 405 | method_not_allowed | The path exists, the method doesn’t. |
| 409 | not_possible, terms_missing | Check now is already running or over its hourly allowance; an invitation before the brand has terms and privacy links. |
| 422 | invalid | Validation: see fields. Also a bad ?limit=, ?cursor= or filter value. |
| 429 | rate_limited | Over a per-key limit; retry_after and the Retry-After header say how many seconds to wait. |
Rate limits and cost
Per key, per minute: 120 reads, 20 writes (POST, PATCH, DELETE) and 10 “check now” calls. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time) for the bucket it counted against. Over the limit you get 429 with Retry-After.
Reading costs nothing. A monitor costs the same credits whether it was made here or in the panel: credits_per_month on every monitor says how many at its interval, and GET /account shows the balance, this month’s use and how many days the credits last. “Check now” is one extra round by every healthy probe, charged like a scheduled round, and limited to once a minute per monitor and 30 an hour per account, shared with the panel.
Checks on hosts outside a domain you have verified keep the panel’s rules too: HTTP(S) and TLS on ports 80 and 443, at most once a minute, no request bodies or custom methods. Verify the domain in the panel to unlock 15-second checks, any port and TCP, SMTP and DNS checks.
Resellers
A reseller account’s key can act inside any of its customer accounts: send X-Account: <customer id or slug> and every endpoint under Reference reads and writes that account instead. Each change is written to the customer’s support log as API key “name”: …, exactly like a visit by one of your team. Customer accounts can also have keys of their own, made by their owner; those see only their account and cannot send X-Account.
The Reseller endpoints list, create and change customer accounts and send invitations. They are available to reseller accounts only and are not combined with X-Account.
Examples
Export every monitor
import requests
API = "https://app.pingwise.com/api/v1"
H = {"Authorization": "Bearer pw_…"}
monitors, cursor = [], None
while True:
r = requests.get(f"{API}/monitors", headers=H, params={"limit": 100, "cursor": cursor})
r.raise_for_status()
page = r.json()
monitors += page["data"]
cursor = page["next_cursor"]
if cursor is None:
break
print(len(monitors), "monitors")
Add a monitor when a site is created, pause it when the site is suspended
r = requests.post(f"{API}/monitors", headers=H, json={
"name": "Customer shop",
"url": "https://shop.example.com",
"interval": 60,
"group": 12,
"notes": "Hosted on web-03. Contact: Nikos, +30 69…",
})
if r.status_code == 422:
print(r.json()["fields"]) # {"url": ["…"]} — the same messages as the panel
monitor_id = r.json()["id"]
requests.post(f"{API}/monitors/{monitor_id}/pause", headers=H)
requests.post(f"{API}/monitors/{monitor_id}/resume", headers=H)
What is down right now?
curl "https://app.pingwise.com/api/v1/incidents?status=open" -H "Authorization: Bearer pw_…"
A reseller creating a customer and its first monitor
acct = requests.post(f"{API}/accounts", headers=H, json={
"name": "Bakery Nikos", "contact_email": "[email protected]", "monthly_cap": 50000,
}).json()
requests.post(f"{API}/monitors", headers={**H, "X-Account": acct["id"]},
json={"url": "https://bakerynikos.gr", "interval": 300})
requests.post(f"{API}/accounts/{acct['id']}/invites", headers=H,
json={"email": "[email protected]", "role": "owner"})
Reference
Generated from openapi.json, the same file the server is tested against: every route below exists, and every route that exists is below. Field details are under Objects.
Account
Monitors
GET /monitors
List monitors Oldest first. Paging with limit=100 and the cursor exports every monitor.
statequery- up, down (down or partial), slow or paused · one of
up,down,slow,paused groupquery- A group id, or none
typequery- A monitor type
qquery- Part of the name, URL or host
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of Monitor
POST /monitors
Add a monitor The same rules as the panel’s New monitor form, with its messages: e.g. other ports than 80 and 443 and some check types need a verified domain. It costs credits_per_month.
Body MonitorInput
Returns 201 Monitor
PATCH /monitors/{id}
Change a monitor Only the fields sent change. On a monitor a hosting panel manages, its customer’s own key may change name, alert_partial and notes only.
Body MonitorInput
Returns 200 Monitor
DELETE /monitors/{id}
Delete a monitor
Returns 204 no body
POST /monitors/{id}/check
Check now One extra round by every healthy probe through the normal quorum, charged like a scheduled round. Once a minute per monitor, 30 an hour per account (shared with the panel), 10 a minute per key.
Returns 202 Check
Incidents
GET /monitors/{id}/incidents
A monitor’s incidents, newest first
statusquery- open or resolved
sincequery- ISO 8601: started or resolved since, or still open
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of Incident
GET /incidents
Incidents, newest first
statusquery- open or resolved · one of
open,resolved sincequery- ISO 8601: started or resolved since, or still open
monitorquery- A monitor id
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of Incident
Groups
GET /groups
Monitor groups
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of Group
DELETE /groups/{id}
Delete a group
Returns 204 no body
Channels
GET /channels
Alert channels (read only; addresses and secrets are never returned)
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of Channel
Status pages
GET /status-pages
Status pages (read only)
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of StatusPage
Probes
Reseller
GET /accounts
Your customer accounts Reseller accounts only (404 otherwise, and while reselling is not switched on). Not with X-Account.
limitquery- Rows per page, 1–100
cursorquery- next_cursor of the previous page
Returns 200 a page of Customer
POST /accounts
Add a customer account Reseller accounts only (404 otherwise, and while reselling is not switched on). Not with X-Account.
Body CustomerInput
Returns 201 Customer
PATCH /accounts/{id}
Change a customer account Reseller accounts only (404 otherwise, and while reselling is not switched on). Not with X-Account. panel_org_id can’t change.
Body CustomerInput
Returns 200 Customer
POST /accounts/{id}/invites
Invite someone to a customer account Reseller accounts only (404 otherwise, and while reselling is not switched on). Not with X-Account. The invitation email the panel sends; 409 terms_missing until your brand has its terms and privacy links.
Body InviteInput
Returns 201 Invite
Meta
GET /openapi.json
This document
Returns 200 object
Objects
Error
| Field | Type | Notes |
|---|---|---|
error required | one of unauthorized, forbidden, account_frozen, not_found, invalid, bad_request, rate_limited, not_possible, terms_missing, method_not_allowed | |
detail | string | |
fields | object | |
retry_after | integer |
MonitorInput
| Field | Type | Notes |
|---|---|---|
name | string | Defaults to the URL or host. · up to 120 characters |
type | one of http, tls, tcp, smtp, dns, ping, heartbeat | default "http" |
url | string or null | http: the full http(s) address. · up to 2000 characters |
host | string or null | tls, tcp, smtp, dns, ping: the hostname. · up to 253 characters |
port | integer or null | 1–65535 |
interval | one of 15, 30, 60, 300, 900, 1800, 3600 | Seconds between checks. · default 60 |
timeout | integer | default 10 · 1–30 |
ip_version | one of auto, 4, 6 | default "auto" |
expect | array of integer | Accepted status codes; [] = any 2xx. |
keyword | string | http: text the page must contain. · up to 200 characters |
keyword_absent | boolean | Down when the keyword IS found. |
follow_redirects | boolean | default true |
verify_tls | boolean | default true |
detect_error_pages | boolean | default true |
cert_warn_days | integer | default 14 · 0–90 |
starttls | boolean | |
expect_banner | string | up to 200 characters |
qname | string | up to 253 characters |
rtype | string | default "A" |
dev_mode | boolean | 401/403/404 count as up, and no alerts are sent. |
method | one of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS | Anything but GET and HEAD is sent from every probe each round and may change data. · default "GET" |
req_headers | array of array of string or object | Shown in the panel. |
req_body | string | up to 10240 characters |
req_content_type | string | |
basic_user | string | up to 200 characters |
hb_period | integer or null | heartbeat: how often the job runs (seconds). |
hb_grace | integer or null | heartbeat: how late it may be (seconds). |
audience | array of one of eu, na, sa, as, af, oc or null | Visitors’ continents; null = the account default. |
slow_ms | integer or null | 50–60000 |
slow_metric | one of ttfb, server, total | |
slow_scope | one of auto, audience, all | |
alert_slow | boolean | |
alert_partial | boolean | |
excluded_continents | array of one of eu, na, sa, as, af, oc or null | Continents whose probes don’t vote; null = the account default. |
group | integer or null | A group id (GET /groups), or null. |
notes | string | Plain text for your team (contact person, what to do). Sent with down alerts and reminders; never on status pages. · up to 2000 characters |
secret_headers | array of array of string or object or null | Write only: stored encrypted, never returned. [] or null removes them. |
basic_password | string or null | Write only: stored encrypted. |
channels | array of string (uuid) | Alert channels that should name this monitor. A channel that alerts for all monitors is left as it is. |
Monitor
| Field | Type | Notes |
|---|---|---|
name | string | Defaults to the URL or host. · up to 120 characters |
type | one of http, tls, tcp, smtp, dns, ping, heartbeat | default "http" |
url | string or null | http: the full http(s) address. · up to 2000 characters |
host | string or null | tls, tcp, smtp, dns, ping: the hostname. · up to 253 characters |
port | integer or null | 1–65535 |
interval | one of 15, 30, 60, 300, 900, 1800, 3600 | Seconds between checks. · default 60 |
timeout | integer | default 10 · 1–30 |
ip_version | one of auto, 4, 6 | default "auto" |
expect | array of integer | Accepted status codes; [] = any 2xx. |
keyword | string | http: text the page must contain. · up to 200 characters |
keyword_absent | boolean | Down when the keyword IS found. |
follow_redirects | boolean | default true |
verify_tls | boolean | default true |
detect_error_pages | boolean | default true |
cert_warn_days | integer | default 14 · 0–90 |
starttls | boolean | |
expect_banner | string | up to 200 characters |
qname | string | up to 253 characters |
rtype | string | default "A" |
dev_mode | boolean | 401/403/404 count as up, and no alerts are sent. |
method | one of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS | Anything but GET and HEAD is sent from every probe each round and may change data. · default "GET" |
req_headers | array of array of string or object | Shown in the panel. |
req_body | string | up to 10240 characters |
req_content_type | string | |
basic_user | string | up to 200 characters |
hb_period | integer or null | heartbeat: how often the job runs (seconds). |
hb_grace | integer or null | heartbeat: how late it may be (seconds). |
audience | array of one of eu, na, sa, as, af, oc or null | Visitors’ continents; null = the account default. |
slow_ms | integer or null | 50–60000 |
slow_metric | one of ttfb, server, total | |
slow_scope | one of auto, audience, all | |
alert_slow | boolean | |
alert_partial | boolean | |
excluded_continents | array of one of eu, na, sa, as, af, oc or null | Continents whose probes don’t vote; null = the account default. |
group | integer or null | A group id (GET /groups), or null. |
notes | string | Plain text for your team (contact person, what to do). Sent with down alerts and reminders; never on status pages. · up to 2000 characters |
id | string (uuid) | |
has_secret_headers | boolean | |
has_basic_password | boolean | |
ping_url | string or null | heartbeat: the URL your job calls. |
channels | array of string (uuid) | Enabled channels that alert for it. |
paused | boolean | |
managed | boolean | Kept in sync by a hosting panel integration. |
state | one of pending, up, slow, down, partial, unconfirmed, blocked, maintenance, paused | |
since | string or null (date-time) | |
last_check_at | string or null (date-time) | |
credits_per_month | integer | What it costs a month at its interval (0 when paused or a heartbeat). |
created_at | string (date-time) | |
updated_at | string (date-time) | |
uptime | object: 24h, 7d, 30d | GET /monitors/{id} only. |
open_incident | integer or null | GET /monitors/{id} only. |
Account
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
name | string | |
slug | string | |
timezone | string | |
billing | string | |
is_reseller | boolean | |
parent | object: id, name or null | |
credits | object: balance, month_used, free_left, daily, days_left | |
plan | object: pack, eur, status, next_renewal_at, ends_at or null | The monthly credit plan, read only; null when there is none. |
Incident
| Field | Type | Notes |
|---|---|---|
id | integer | |
monitor | object: id, name | |
status | one of open, resolved | |
reason | string | |
detail | string | |
started_at | string (date-time) | |
confirmed_at | string (date-time) | |
resolved_at | string or null (date-time) | |
duration_s | integer or null | |
explanation | object: headline, what_happened, likely_cause, what_to_do, scope, confidence or null | Accounts with AI explanations only. |
Group
| Field | Type | Notes |
|---|---|---|
id | integer | |
name | string | up to 120 characters |
position | integer | |
monitors | integer |
GroupInput
| Field | Type | Notes |
|---|---|---|
name | string | up to 120 characters |
position | integer | 0 |
Channel
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
kind | string | |
name | string | |
enabled | boolean | |
verified | boolean | |
all_monitors | boolean | |
monitors | array of string (uuid) | |
groups | array of integer |
StatusPage
| Field | Type | Notes |
|---|---|---|
id | integer | |
name | string | |
slug | string | |
url | string | |
visibility | one of public, protected, private | |
brand | object: id, name | |
entries | array of object: id, name, section, group, monitors |
Probe
| Field | Type | Notes |
|---|---|---|
name | string | |
location | string | |
continent | string | |
provider | string | |
ipv4 | string or null | |
ipv6 | string or null | |
rdns | string or null |
Check
| Field | Type | Notes |
|---|---|---|
id | integer | |
monitor | string (uuid) | |
status | string | |
requested_at | string (date-time) |
Customer
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
slug | string | |
name | string | |
contact_email | string or null | |
monthly_cap | integer or null | |
month_used | integer | |
monitors | integer | |
open_incidents | integer | |
panel_org_id | string or null | |
joined | boolean | |
invited | boolean | |
created_at | string (date-time) |
CustomerInput
| Field | Type | Notes |
|---|---|---|
name | string | up to 150 characters |
contact_email | string or null (email) | |
monthly_cap | integer or null | Credits a month this customer may use; null = no cap. · 1 |
panel_org_id | string | POST only: an Enhance organisation whose websites are in your account; they move to the new customer and stay in sync. |
InviteInput
| Field | Type | Notes |
|---|---|---|
email required | string (email) | |
role | one of owner, member, viewer | default "owner" |
Invite
| Field | Type | Notes |
|---|---|---|
id | integer | |
account | string (uuid) | |
email | string | |
role | string | |
created_at | string (date-time) | |
expires_in_days | integer |
Changes
v1 · October 2026: first release. We add fields and endpoints without changing what exists; anything that would break a client gets a new version path and notice in the panel.
Questions or a missing endpoint: hello@pingwise.com.