API Reference
Pingniner exposes three separate things, and it helps to know which one you want:
| Called by | Purpose | |
|---|---|---|
| Management API | Your code | Read and manage monitors |
| Heartbeat ping | Your code | Report that a scheduled job ran |
| Agent ingest | The agent | Report server and workstation metrics |
The management API is authenticated with an API token. The two ingest endpoints are not — the key in the URL is the credential.
If you would rather not write HTTP calls at all, Claude Code can drive the management API for you.
Management API
https://app.pingniner.com/api/v1Every request needs an API token as a bearer token:
curl https://app.pingniner.com/api/v1/monitors \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/json"The token decides which account you are working on. There is no account parameter, and adding one changes nothing.
The seven monitor types
Wherever an endpoint takes a {type}, it is one of:
site · server · workstation · heartbeat · port · ping · blacklist
They do not all behave alike, and two differences matter when writing against the API:
- Sites, ports, pings and blacklists are checked by Pingniner from a monitoring location, on an interval your plan allows. They need a
location_idand afrequency. - Heartbeats work the other way round: your job calls us. Their
frequencydescribes your schedule, not our checking, and is not limited by your plan. - Servers and workstations report from an installed agent. They have metrics rather than uptime, and creating one only makes the record — nothing is monitored until the agent is installed.
Endpoints
| Method | Path | Needs |
|---|---|---|
GET | /account | Read |
GET | /monitors | Read |
GET | /monitors/{type}/{id} | Read |
GET | /monitors/{type}/{id}/history | Read |
GET | /monitors/{type}/{id}/uptime | Read |
GET | /alerts | Read |
GET | /servers/{id}/metrics | Read |
GET | /locations | Read |
GET | /groups | Read |
POST | /monitors/{type} | Write |
PATCH | /monitors/{type}/{id} | Write |
POST | /monitors/{type}/{id}/pause | Write |
POST | /monitors/{type}/{id}/resume | Write |
There is deliberately no delete endpoint. See API tokens.
Account and limits
GET /account is worth calling before you create anything — it tells you whether your plan has room and which check intervals it allows.
{
"data": {
"id": 42,
"name": "Acme Ltd",
"subscribed": true,
"monitors": { "used": 12, "limit": 30, "can_add": true },
"status_pages": { "used": 1, "limit": 3 },
"default_retention_days": 30,
"available_check_intervals_minutes": [1, 2, 3, 4, 5, 10, 15, 30, 60, 1440]
}
}available_check_intervals_minutes is the complete set of values frequency will accept when creating or updating a site, port, ping or blacklist. Anything else is refused.
TIP
On the plans that offer sub-minute checking the list starts with 0, which means every 30 seconds rather than zero minutes. It is the one value in there that is not a count of minutes.
Listing monitors
GET /monitors returns every monitor, grouped by type. Without a type filter all seven keys are present, including empty arrays for the types you do not use — so you can index data.site without checking it exists first.
Filters: type, status, group_id, tag, search.
curl "https://app.pingniner.com/api/v1/monitors?type=site&status=Down" \
-H "Authorization: Bearer YOUR_TOKEN"{
"data": {
"site": [
{
"id": "9d1f2c34-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"type": "site",
"name": "Marketing site",
"status": "Up",
"paused": false,
"target": { "url": "https://example.com" },
"group_id": null,
"tags": ["production"],
"retention_days": 30,
"last_seen_at": "2026-08-28T14:32:00+00:00",
"created_at": "2026-01-04T09:12:00+00:00",
"location_id": 3,
"check_interval_minutes": 5
}
]
},
"meta": { "total": 1 }
}target is shaped by type, so you can tell a URL from a hostname without guessing: {"url": …} for sites, {"host": …, "port": …} for ports, {"host": …} for pings, {"ip_address": …} for blacklists, and {} for the three with no external target.
Heartbeats add expected_every_minutes and grace_minutes. Servers and workstations add platform, agent_key and agent_reporting.
Status, and what "paused" means
status is the monitor's health: Up, Down, Degraded, Maintenance or Unknown.
A monitor with status Maintenance is paused, which the paused field also reports plainly. Read that carefully:
WARNING
Pausing suppresses alerting. It does not stop the checks. Pingniner keeps contacting the target and keeps recording history — only the alerts stop. If you need us to stop touching a host entirely, pausing is not the tool.
History
GET /monitors/{type}/{id}/history returns individual check results, newest first.
Parameters: limit (default 100, maximum 1000), from, to.
Readings differ by type, so they are nested under readings rather than pretending to a common shape:
{
"data": [
{
"id": 91827364,
"timestamp": "2026-08-28 14:32:00",
"location_id": 3,
"readings": {
"latency": 187,
"online": 1,
"http_status_code": 200,
"namelookup_time": 12,
"connect_time": 41,
"pretransfer_time": 88,
"starttransfer_time": 175
}
}
]
}Ask for the smallest window that answers your question. These are the largest tables in Pingniner, and the response deliberately leaves out the heaviest columns — a site's full response body, a server's per-disk and per-process JSON.
Uptime
GET /monitors/{type}/{id}/uptime?period=week, where period is today, week, month or year. Defaults to week.
{
"data": {
"monitor_id": "9d1f2c34-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"monitor_type": "site",
"period": "week",
"uptime_percent": 99.9821
}
}Servers and workstations return 422 — they record metrics rather than up/down checks, so there is no uptime figure to give. Use /servers/{id}/metrics instead.
Alerts
GET /alerts returns alerts grouped by monitor type, newest first. An alert with "open": true has not ended yet, which is what to look at for what is wrong right now.
Parameters: type, limit (default 50, maximum 500), open.
curl "https://app.pingniner.com/api/v1/alerts?open=true" \
-H "Authorization: Bearer YOUR_TOKEN"Creating a monitor
POST /monitors/{type} with the fields that type needs.
curl -X POST https://app.pingniner.com/api/v1/monitors/site \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Marketing site",
"url": "https://example.com",
"location_id": 3,
"frequency": 5
}'| Type | Required |
|---|---|
site | name, url, location_id, frequency |
port | name, host, port, location_id, frequency |
ping | name, host, location_id, frequency |
blacklist | name, ip_address, location_id, frequency |
heartbeat | name, frequency, grace |
server, workstation | name, type (Linux, Windows, macOS or BSD) |
Optional on every type: group_id, tags, notes. Sites also accept method, expected_status_code, timeout, connect_timeout, useragent, postdata, headers, additional_locations, certificate_check, domain_check, broken_links_check, lighthouse_check and lighthouse_schedule.
Get location_id from GET /locations and group_id from GET /groups.
A new monitor arrives configured exactly as one created in the interface — the same default triggers, the same retention from your plan, and its first check already queued.
Fields that Pingniner itself writes — status, last_checked, certificate details, agent readings — are ignored if you send them. You cannot declare your own site up.
TIP
Creating a server or workstation creates the record only. Nothing is monitored until the agent is installed on the machine using the agent_key in the response.
Updating, pausing and resuming
PATCH /monitors/{type}/{id} changes only the fields you send.
curl -X PATCH https://app.pingniner.com/api/v1/monitors/site/YOUR_ID \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"frequency": 1}'POST /monitors/{type}/{id}/pause and .../resume move a monitor in and out of maintenance. Both return the monitor and a note describing what actually changed.
Errors
| Status | Means |
|---|---|
401 | No token, or an expired or revoked one |
403 | The token lacks the ability this endpoint needs, or is no longer valid for its account |
404 | No such monitor on this account |
422 | Understood but refused — validation, or a plan limit |
429 | Rate limited |
A monitor belonging to another account returns 404, not 403. Telling you that you may not see something still confirms it exists.
Plan limits come back as 422 with the numbers, so you can say what to do about it rather than only that it failed:
{
"message": "This account is using 30 of its 30 available monitors. Remove a monitor or upgrade the plan to add another.",
"monitors": { "used": 30, "limit": 30 }
}Validation failures use the standard shape:
{
"message": "The frequency field is invalid.",
"errors": { "frequency": ["The selected frequency is invalid."] }
}A common cause is asking for a check interval your plan does not include — call GET /account for the ones it does.
Heartbeat ping
This is the endpoint you integrate with to report that a scheduled job ran. Full documentation with code examples for cron, PHP, Python, Ruby, Node.js, Perl and Bash is on the Heartbeats page; this is the reference.
Endpoint
GET https://ingest.pingniner.com/api/heartbeat/{key}{key} is the heartbeat monitor's ID, shown on its Overview page.
Optional query parameters
| Parameter | Type | Description |
|---|---|---|
runtime | number | How long the job took, in seconds |
memory | number | Peak memory used, in MB |
message | string | A short note, URL-encoded |
Example
curl -fsS -m 10 --retry 5 -o /dev/null \
"https://ingest.pingniner.com/api/heartbeat/YOUR_KEY?runtime=42&memory=128&message=Backup%20complete"Response
{ "success": true, "message": "Received successfully" }An unknown key returns:
{ "success": false, "message": "Heartbeat not found" }Authentication: none. The key in the URL is the credential — this is not an API token, and the two are not interchangeable.
Pingniner also records the source IP address and user agent of each ping, both visible in the heartbeat's history.
TIP
Call the ping URL after your job succeeds, not at the start. A heartbeat that fires before the work happens only proves the job started.
Agent ingest
These endpoints exist for the agent and are documented for completeness. You should not need to call them yourself — install the agent instead.
POST https://ingest.pingniner.com/api/server/{key}
POST https://ingest.pingniner.com/api/workstation/{key}{key} is the server or workstation monitor's key, which is also the agent_key returned when you create one through the management API. The body is a JSON payload produced by the systeminformation library — see What the Agent Collects for the fields.
Authentication: none. The key in the URL is the credential.
WARNING
Because these endpoints are unauthenticated beyond the key, anyone holding a monitor key can post data to that monitor. Treat keys like passwords: do not paste install commands containing real keys into tickets, shared documents or chat.
Outbound webhooks
Pingniner can call your endpoint when an incident opens or closes. That is configured per user as a notification channel, not here.
The request is a POST with a User-Agent of Pingniner and a body of:
{
"message": "Incident: My Website - Offline @ 2026-08-27 14:32:00"
}Use this to reach anything without a built-in integration — PagerDuty, Opsgenie or your own automation. Slack, Microsoft Teams, Google Chat and Discord all have dedicated channels of their own; see Notification Channels.
Status page subscriptions
Status page subscribers manage themselves through the public page. There is no endpoint for adding subscribers programmatically, deliberately — subscriptions are double opt-in so that nobody can be signed up by someone else.
Network requirements
Ingest goes to one host, the management API to another:
| Host | Port | Direction | Used by |
|---|---|---|---|
ingest.pingniner.com | 443 (HTTPS) | Outbound from your infrastructure | Heartbeat pings, the agent |
app.pingniner.com | 443 (HTTPS) | Outbound from your infrastructure | Management API, Claude Code |
Add them to any egress allowlist. Nothing needs to be open inbound.
If you are allowlisting Pingniner's outbound checks — the addresses our site, ping and port monitors connect from — see the IP list in the FAQ.
