The REST API
Automate CompleteStatus with the JSON REST API — create API keys and scopes, manage monitors and incidents, and mute alerts during deploys from CI.
On this page
CompleteStatus's REST API lets you manage monitors, read uptime, read and acknowledge incidents, and mute alerts during deploys — everything you need to wire monitoring into CI/CD.
Note: API access requires the Business plan or above. API keys can only be created once your organization is on a plan that includes API access — the "Create key" action is unavailable on Free and Pro.
Creating an API key
- Go to /settings/api-keys (you must be an organization owner or admin).
- Give the key a name (e.g. "CI deploy"), pick its scopes, and optionally set an expiry in days.
- Click create. The plaintext token is shown exactly once — copy it now. CompleteStatus stores only a SHA-256 hash, so the token can never be displayed again.
Tokens look like wt_a1b2c3d4_… (a wt_ prefix, 8 random characters, an underscore, then 40 random characters). Revoke a key at any time from the same page; the list shows each key's scopes, expiry and last-used time.
Scopes
| Scope | Grants |
|---|---|
monitors:read |
List monitors and monitor groups, read one, read its check results and uptime |
monitors:write |
Create, update, delete, pause, resume, trigger checks |
incidents:read |
List and read incidents |
incidents:write |
Acknowledge and resolve incidents, post status-page updates, open manual incidents |
maintenance:write |
Create and end maintenance windows |
hooks:manage |
Subscribe, list and delete automation hooks |
mcp |
Use the MCP server |
Grant the minimum a job needs — a deploy pipeline usually only needs maintenance:write.
Authentication
Send the token on every request, either way works:
curl -H "Authorization: Bearer wt_xxxxxxxx_..." https://completestatus.com/api/v1/ping
# or
curl -H "X-Api-Key: wt_xxxxxxxx_..." https://completestatus.com/api/v1/ping
All data is scoped to the key's organization — a key can never see another tenant's data. Verify a key with GET /api/v1/ping (alias /api/v1/me):
{
"authenticated": true,
"organization": { "id": 1, "name": "Acme Inc.", "plan": "business",
"effective_plan": "business", "comped": false },
"api_key": { "id": 3, "name": "CI deploy", "prefix": "wt_a1b2c3d4",
"scopes": ["maintenance:write"], "expires_at": null }
}
effective_plan is the plan your limits and features come from. Read it when you need to know what the key can do. plan is the plan you are billed for. The two differ only on a complimentary plan: comped is then true and effective_plan names that plan.
Rate limits and errors
Each key may make 120 requests per minute; beyond that you get 429 with a Retry-After header. Other errors: 401 (missing/invalid/expired key), 403 (plan doesn't include API access, the key lacks a required scope, or the team member who created the key is suspended because the organization's plan no longer covers their membership — the key works again, unchanged, once they are restored), 404 (resource not in your organization), 422 (validation or plan-limit failure, e.g. monitor cap reached or interval below your plan's minimum). The monitor cap counts running monitors only, so creating or resuming a monitor at the cap is refused while pausing one frees a slot.
Monitors
GET /api/v1/monitors ?type=&status=&project_id=&monitor_group_id=&per_page= (paginated, default 25)
POST /api/v1/monitors
GET /api/v1/monitors/{id}
PATCH /api/v1/monitors/{id}
DELETE /api/v1/monitors/{id} → 204
POST /api/v1/monitors/{id}/check → 202, queues a check now
POST /api/v1/monitors/{id}/pause
POST /api/v1/monitors/{id}/resume
GET /api/v1/monitors/{id}/results ?limit= (1–200, default 50)
DELETE also removes the monitor from maintenance windows that have not ended yet, including ad-hoc mutes. A window that covered only that monitor is removed. If the monitor has an open incident, the incident is resolved first and the recovery goes to the alert channels that received the down alert. The monitor's incident history is kept.
Create a monitor (requires monitors:write):
curl -X POST https://completestatus.com/api/v1/monitors \
-H "Authorization: Bearer $WT_TOKEN" -H "Content-Type: application/json" \
-d '{
"project_id": 1,
"type": "uptime",
"name": "Marketing site",
"target": "https://example.com",
"interval_sec": 300,
"ownership_attested": true
}'
Pass monitor_group_id on create or update to put the monitor in a monitor group — it must be a group of the monitor's project, otherwise you get 422. Send "monitor_group_id": null to make it Ungrouped. Moving a monitor to another project without a monitor_group_id makes it Ungrouped (groups never span projects).
interval_sec must be one of 30, 60, 180, 300, 900, 1800, 3600, 21600, 43200, 86400 (every 30 seconds up to once a day). Two minimums still apply on top: your plan's fastest interval, and the monitor type's own minimum (for example, a security-headers monitor runs at most every 6 hours). A faster value is refused with 422. ownership_attested: true is required on create — you must own or be authorized to monitor the target. Changing target on PATCH /api/v1/monitors/{id} needs ownership_attested: true too (422 otherwise), and ownership_attested_at is re-stamped when the target changes; resending the unchanged target needs no attestation. type cannot be changed after creation.
regions (optional, on create or update) lists the monitoring locations to check the monitor from, as region codes. The home region, us-east, is always part of the set: it is added if you leave it out, and it counts toward your plan's per-monitor location limit, so a set that would go over the limit is refused with 422. Only uptime, api, keyword, headers, port, cert and tls_config monitors can use other locations. Every other type runs from the home region only, whatever you send.
name must be unique within your organization, ignoring case. A duplicate is refused with 422 on name. A request whose text is not valid UTF-8 always gets a JSON 422 with the error under errors.encoding.
A monitor created through the API gets a default email alert rule, the same as one created in the app: down and up alerts, plus the extra events of its type, sent to the email address of the user who created the API key (no rule is added if that user has left the organization). Send "notify_email": false on create to make the monitor with no alert rule (default true).
Response (201):
{
"data": {
"id": 42, "project_id": 1, "monitor_group_id": null, "group": null, "type": "uptime",
"name": "Marketing site", "target": "https://example.com",
"status": "pending", "interval_sec": 300,
"consecutive_failures": 0, "consecutive_successes": 0,
"last_checked_at": null, "next_check_at": "2026-07-04T10:00:00+00:00",
"ownership_attested_at": "2026-07-04T10:00:00+00:00",
"created_at": "2026-07-04T10:00:00+00:00", "updated_at": "2026-07-04T10:00:00+00:00"
}
}
Updating a monitor
PATCH /api/v1/monitors/{id} changes only the fields you send. Any change to the target, interval, regions or config of a running monitor makes it due on the next scheduler tick. The response has a top-level warnings array next to data, usually empty:
- Moving a monitor to another project while its alert rules still use the previous project's channels adds a warning. The rules keep working and are not changed; re-point them in the app if you want.
- Changing a content-change monitor's target, rendering, CSS selector or ignore patterns (a DNS monitor's hostname or record types, or a certificate-transparency monitor's domain) adds a warning that its baseline will be re-captured on the next check, so the edit itself is never reported as a change.
Changing project_id never rewrites the monitor's uptime history. Time before the move keeps the old project's maintenance windows, and the new project's windows apply from the move on. The monitor's own active mute moves with it. See Moving a monitor to another project.
For most types, a config you send replaces the stored config as a whole. Heartbeat monitors are different: a config you send is merged into the existing one, so {"config": {"grace_sec": 300}} keeps a cron schedule. Changing a heartbeat's interval_sec or config updates its live ping schedule at once. The heartbeat config keys are:
| Key | Accepted values |
|---|---|
schedule_mode |
interval or cron |
cron_expression |
A five-field crontab line (e.g. 0 3 * * *), or a shortcut such as @daily. Required in cron mode. Six- or seven-field (Quartz) lines and @reboot are refused. |
cron_timezone |
An IANA timezone such as America/New_York. Default UTC. |
grace_sec |
0–86400 |
max_duration_sec |
1–86400 |
expected_interval_sec |
One of the interval_sec values above |
An invalid value returns 422 with the error on config.<key>.
Check now
POST /api/v1/monitors/{id}/check queues a check and returns 202 with a queued boolean. Calls for the same monitor within 15 seconds of a queued check are de-duplicated: they return 202 with "queued": false, because one check is already on its way. Heartbeat monitors are updated by their pings, not by checks, so check on a heartbeat returns 422.
Pause and resume
pause stops a monitor's checks until it is resumed, and returns the monitor. Pausing a monitor that is already paused keeps it paused. Pausing a monitor that is down resolves its open incident at that moment, without an alert.
resume only acts on a paused monitor: it goes back to pending and is checked on the next scheduler tick, with its failure count starting from zero. A resumed heartbeat monitor's expected-ping window starts again from the moment of the resume, so a long pause does not make it overdue straight away. On a monitor that is not paused (up, down or pending), resume changes nothing and returns the monitor as it is (200). A deploy script can therefore call resume unconditionally without resetting a monitor mid-incident. A paused monitor whose type your plan no longer includes (for example a transaction monitor after a downgrade) cannot be resumed: resume returns 422 and the monitor stays paused. Resuming also needs a free slot under your plan's monitor limit, which counts running monitors only (paused monitors never use a slot): when every slot is in use, resume returns 422 and the monitor stays paused. Pause another monitor first, or upgrade.
Check results
GET /api/v1/monitors/{id}/results returns the most recent checks, newest first, from within the history your plan shows. Each result has ok, probe_region, status_code, latency_ms, ttfb_ms, ran_at, detail (the check's findings, which depend on the monitor type) and detail_unchanged.
Security-headers and page-health monitors store a run's full findings only when they change. When a run matches the one before it, detail_unchanged is true and detail holds the findings of that earlier run, which are also this run's findings. So detail carries the findings either way. In the rare case that the earlier run is no longer stored, detail is just a short marker instead: {"unchanged": true, "grade": ...}.
Monitor groups
Groups bucket similar infrastructure inside a project (e.g. "Web servers", "Databases"); walls and the monitor list use them to cluster monitors. Groups are created, renamed, reordered and deleted in the app (Projects → Groups); the API lists them read-only (requires monitors:read):
GET /api/v1/monitor-groups ?project_id=
{
"data": [
{ "id": 7, "project_id": 1, "name": "Web servers", "position": 0, "monitors_count": 4,
"created_at": "2026-09-27T10:00:00+00:00", "updated_at": "2026-09-27T10:00:00+00:00" }
]
}
Groups come back in display order. Each monitor's monitor_group_id and group (the group name, or null for Ungrouped) are included on every monitor response.
Uptime
Read the same uptime figures your status pages, walls and SLA reports show (requires monitors:read):
GET /api/v1/uptime ?days= &project_id= &monitor_ids=
GET /api/v1/monitors/{id}/uptime ?days= &daily=1
daysis the window: the last N calendar days in your organization's timezone, today so far included. 1–365, default 30. It never reaches further back than the history your plan shows: a longerdaysis cut to it.window.daysis the number of days actually measured, andwindow.history_daysis your plan's history window.GET /api/v1/uptimereturns the figure across monitors (the simple average of each monitor's own uptime) plus every monitor's own. Narrow it withproject_idormonitor_ids(a comma list such as3,7,12, or an array).GET /api/v1/monitors/{id}/uptimereturns one monitor. Setdaily(e.g.daily=1) to add the day-by-day breakdown, the status-page bars as data.
Uptime is time-based: the share of monitored time without a confirmed outage. An outage counts from when its incident opened until it resolved, so a single failed check that passes on the re-check is not downtime. Maintenance windows and paused time count as neither up nor down.
curl "https://completestatus.com/api/v1/monitors/42/uptime?days=7&daily=1" \
-H "Authorization: Bearer $WT_TOKEN"
{
"data": {
"monitor_id": 42, "name": "Marketing site",
"window": { "from": "2026-09-21T00:00:00+00:00", "to": "2026-09-27T14:05:00+00:00",
"timezone": "UTC", "days": 7, "history_days": 730 },
"uptime_pct": 99.95, "observed_seconds": 569100, "downtime_seconds": 240,
"maintenance_seconds": 0,
"daily": [
{ "date": "2026-09-21", "uptime_pct": 100.0, "state": "up",
"downtime_seconds": 0, "maintenance_seconds": 0 }
]
}
}
GET /api/v1/uptime returns window, uptime_pct, monitors_measured (how many monitors had monitored time in the window) and monitors, one entry per monitor with its monitor_id, name, uptime_pct, observed_seconds, downtime_seconds and maintenance_seconds.
uptime_pctis the displayed value: two decimals, truncated rather than rounded up, and never100when there was any downtime. It isnullwhen nothing was monitored in the window (for example, the monitor was paused throughout).- The
*_secondsfields are the exact inputs, if you want your own precision. - A day's
stateisup,partial(under 30 minutes of confirmed downtime),down(30 minutes or more),maintenanceornodata.
Incidents
GET /api/v1/incidents ?status=open|acknowledged|resolved|all (default open) &monitor_id=&per_page=
GET /api/v1/incidents/{id}
POST /api/v1/incidents/{id}/ack
POST /api/v1/incidents/{id}/resolve
Acknowledge from a chatops script (requires incidents:write):
curl -X POST https://completestatus.com/api/v1/incidents/17/ack \
-H "Authorization: Bearer $WT_TOKEN"
Acknowledgements and resolutions are stamped on the incident timeline as api:<key name>.
Resolving an automatic incident with POST /api/v1/incidents/{id}/resolve also sends its recovery, marked as a manual resolve, to the alert channels that received the down alert, to status-page subscribers and to automation hooks (incident.resolved). If the monitor is still down, it goes back to pending: a check that passes moves it to up, and failures that are confirmed again open a new incident.
Every incident carries its status-page fields: source (auto for monitor-opened, manual for human-opened), title, public_title, impact (none, degraded_performance, partial_outage, major_outage — manual incidents), public_status (investigating, identified, monitoring, resolved, or null for an automatic incident nobody has written about yet), project_id and affected_monitor_ids (manual incidents) and latest_update.
Status-page updates and manual incidents
GET /api/v1/status-pages (incidents:read)
POST /api/v1/incidents (incidents:write) open a manual incident
GET /api/v1/incidents/{id}/updates (incidents:read) newest first
POST /api/v1/incidents/{id}/updates (incidents:write) → 201
Post a public update on an incident. status is investigating, identified, monitoring, resolved, or update (adds information without changing the status). The message is plain text shown publicly: line breaks are kept, links become clickable, and HTML is displayed as typed. Set notify to true to email the status page's confirmed subscribers. Each update is emailed at most once, and editing it later never sends it again.
curl -X POST https://completestatus.com/api/v1/incidents/17/updates \
-H "Authorization: Bearer $WT_TOKEN" -H "Content-Type: application/json" \
-d '{"status": "identified", "message": "A bad deploy. Rolling back now.", "notify": true}'
On an automatic incident whose monitor is still failing, resolved is refused with a 422. The monitor state machine owns resolution, so the status page never says "resolved" while checks still fail. Post monitoring for now. Once the monitor recovers, or you resolve the incident, resolved works as a closing note. On a manual incident, resolved resolves it, and a later investigating/identified/monitoring reopens it.
Open a manual incident (no monitor) on one or more status pages of one project. Use GET /api/v1/status-pages for page ids and the component monitor ids each page lists:
curl -X POST https://completestatus.com/api/v1/incidents \
-H "Authorization: Bearer $WT_TOKEN" -H "Content-Type: application/json" \
-d '{"title": "Payments are slow", "impact": "degraded_performance",
"status_page_ids": [3], "affected_monitor_ids": [12],
"status": "investigating", "message": "We are looking into slow payments."}'
A manual incident never pages your alert channels or fires automation hooks. Subscribers are emailed only when you set notify. POST /api/v1/incidents/{id}/resolve on a manual incident posts a (non-emailing) "Resolved" update.
Maintenance windows (mute during deploys)
POST /api/v1/maintenance-windows
DELETE /api/v1/maintenance-windows/{id} → 204
Mute a project's alerts for the length of a deploy (requires maintenance:write):
curl -X POST https://completestatus.com/api/v1/maintenance-windows \
-H "Authorization: Bearer $WT_TOKEN" -H "Content-Type: application/json" \
-d '{"project_id": 1, "duration_min": 15, "reason": "Deploying v2"}'
duration_min defaults to 30 and is capped at 1440 (24 h). Pass monitor_ids to mute only specific monitors; omit it to cover the whole project. Incidents are still recorded during a window — only the notifications are suppressed — and the window's time is left out of uptime % (see maintenance windows). To end the mute early, delete the window:
curl -X DELETE https://completestatus.com/api/v1/maintenance-windows/WINDOW_ID \
-H "Authorization: Bearer $WT_TOKEN"
Deleting a window that has already started ends it now and keeps it on record, so the downtime inside it stays excluded from uptime. Deleting a window that hasn't started yet removes it. Deleting a window that has already finished changes nothing. Each case returns 204.
Client examples
The API is plain JSON over HTTPS, so any HTTP client works — the examples below use each language's most common one. All four snippets do the same thing: authenticate with the Authorization: Bearer header (the X-Api-Key header shown under Authentication above works identically), list monitors, create a heartbeat monitor, then pause and resume it. For heartbeat monitors, interval_sec is the expected ping interval and config.grace_sec the grace period in seconds; the API response doesn't include the ping token, so open the monitor in the UI to copy its ping URL. Keep the token itself in an environment variable, never in code.
PHP
Plain PHP with the curl extension (in a Laravel app, Http::withToken($token)->get(...) does the same in one line). The helper below throws on any HTTP error so a failing call can't be silently ignored.
<?php
function wt(string $method, string $path, ?array $body = null): array
{
$ch = curl_init('https://completestatus.com/api/v1'.$path);
$headers = [
'Authorization: Bearer '.getenv('WT_TOKEN'),
'Accept: application/json',
];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => $headers,
]);
$json = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($json === false || $status >= 400) {
throw new RuntimeException("CompleteStatus API error: HTTP {$status} {$json}");
}
return json_decode($json, true) ?? [];
}
// List monitors (requires monitors:read)
$monitors = wt('GET', '/monitors')['data'];
// Create a heartbeat monitor (requires monitors:write)
$monitor = wt('POST', '/monitors', [
'project_id' => 1,
'type' => 'heartbeat',
'name' => 'Nightly backup',
'target' => 'nightly-backup',
'interval_sec' => 86400,
'config' => ['grace_sec' => 1200],
'ownership_attested' => true,
])['data'];
// Pause and resume
wt('POST', "/monitors/{$monitor['id']}/pause");
wt('POST', "/monitors/{$monitor['id']}/resume");
Python
Using requests with a Session, so the auth header is set once. raise_for_status() turns 4xx/5xx responses into exceptions.
import os
import requests
BASE = "https://completestatus.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['WT_TOKEN']}"
def wt(method, path, json=None):
resp = session.request(method, BASE + path, json=json, timeout=15)
resp.raise_for_status()
return resp.json() if resp.content else {}
# List monitors (requires monitors:read)
monitors = wt("GET", "/monitors")["data"]
# Create a heartbeat monitor (requires monitors:write)
monitor = wt("POST", "/monitors", json={
"project_id": 1,
"type": "heartbeat",
"name": "Nightly backup",
"target": "nightly-backup",
"interval_sec": 86400,
"config": {"grace_sec": 1200},
"ownership_attested": True,
})["data"]
# Pause and resume
wt("POST", f"/monitors/{monitor['id']}/pause")
wt("POST", f"/monitors/{monitor['id']}/resume")
Node.js
Built-in fetch (Node 18+), no dependencies. The wrapper rejects on non-2xx responses so errors surface as exceptions.
const BASE = "https://completestatus.com/api/v1";
async function wt(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${process.env.WT_TOKEN}`,
Accept: "application/json",
...(body && { "Content-Type": "application/json" }),
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) {
throw new Error(`CompleteStatus API error: HTTP ${res.status} ${await res.text()}`);
}
return res.status === 204 ? null : res.json();
}
// List monitors (requires monitors:read)
const { data: monitors } = await wt("GET", "/monitors");
// Create a heartbeat monitor (requires monitors:write)
const { data: monitor } = await wt("POST", "/monitors", {
project_id: 1,
type: "heartbeat",
name: "Nightly backup",
target: "nightly-backup",
interval_sec: 86400,
config: { grace_sec: 1200 },
ownership_attested: true,
});
// Pause and resume
await wt("POST", `/monitors/${monitor.id}/pause`);
await wt("POST", `/monitors/${monitor.id}/resume`);
Related guides
- MCP server — the same capabilities, exposed to AI assistants.
- Cron and heartbeat monitors — the public heartbeat ping endpoint.
- Teams and organizations — who can manage API keys.
Ready to try it?
10 monitors, security grading and email-authentication checks on the free tier — commercial use allowed.