Integrations

Developer API

Authenticate, list monitors, fetch incidents, and stream check results from Pingara programmatically using the v1 REST Developer API.

13 min readUpdated August 2, 2026
apideveloperrestintegrationautomation

The Pingara Developer API gives you programmatic access to your monitors, incidents, and check results over HTTPS. It's designed for CI/CD pipelines, internal dashboards, ChatOps tools, and any other system that needs to read monitoring state without screen-scraping the UI.

At a glance

  • Base URL (production): https://api.pingara.io/api/v1
  • Base URL (development): https://api-dev.pingara.io/api/v1
  • Auth: Bearer token (Authorization: Bearer pgr_…)
  • Format: JSON, UTF-8
  • Scope: v1 is read-only and scoped to a single organization per key
  • Rate limit: 600 requests per minute, per key
  • Versioning: All endpoints live under /api/v1. Breaking changes ship under /api/v2.

1. Create an API key

  1. Sign in to Pingara and switch to the organization you want to script against.
  2. Open Settings → API.
  3. Click Create key, give it a recognisable name (for example CI deploy or Grafana sync), and submit.
  4. Copy the full key immediately. It is shown exactly once and starts with the prefix pgr_. Pingara only stores a SHA-256 hash, so a lost key cannot be recovered — revoke it and create a new one.

Only owners and admins can create or revoke keys. Each organization can hold up to 25 active keys at a time.

Storing keys safely

  • Treat the key like a password. Anyone with it can read every monitor, incident, and check result in your organization.
  • Store keys in your secrets manager (1Password, Vault, AWS Secrets Manager, GitHub Actions secrets, etc.) — never in source control.
  • Use a dedicated key per integration so you can revoke one without breaking the rest.

2. Authentication

Every request must include an Authorization header:

curl https://api.pingara.io/api/v1/me \
  -H "Authorization: Bearer pgr_abcdef0123456789..."

Successful responses return JSON with HTTP 200. Authentication failures return 401:

{ "error": "Unauthorized" }

This message is deliberately identical for every failure mode — a missing header, the wrong scheme, a malformed key, or a key that doesn't match any hash in your organization. Pingara never returns a more specific reason, so a caller can't probe which case they hit to learn whether a given key exists. Don't branch your error handling on the message text; treat any 401 as "not authenticated" and re-check the header.

3. Resources

GET /api/v1/me

Returns metadata about the organization the key belongs to.

{
  "organization": {
    "id": "j5k2…",
    "name": "Acme Inc",
    "slug": "acme",
    "plan": "pro"
  },
  "apiKey": {
    "id": "abcd…",
    "scopes": ["read"]
  }
}

Useful as a health-check at the start of a CI job.

GET /api/v1/monitors

List monitors in the organization, most recently updated first.

Query paramTypeDescription
limitinteger (1–200)Maximum monitors to return. Default 50.
statusstringFilter to one of up, down, degraded, pending, paused.

Example response:

{
  "count": 2,
  "data": [
    {
      "id": "m1aa…",
      "name": "Marketing site",
      "url": "https://www.example.com",
      "type": "http",
      "port": null,
      "tcpProtocol": null,
      "method": "GET",
      "interval": "1m",
      "timeout": 10000,
      "regions": ["us-east-1", "eu-west-1"],
      "expectedStatusCodes": [200, 301, 302],
      "keywordCheck": null,
      "keywordCheckEnabled": false,
      "apdexThreshold": 500,
      "tags": ["public", "marketing"],
      "environment": "production",
      "service": "web",
      "criticality": "important",
      "status": "up",
      "isEnabled": true,
      "isPaused": false,
      "lastCheckedAt": "2026-05-14T16:24:09.123Z",
      "lastStatusChange": "2026-05-12T09:11:02.000Z",
      "sslExpiresAt": "2026-06-16T23:59:59.000Z",
      "sslDaysUntilExpiry": 33,
      "createdAt": "2026-01-04T12:00:00.000Z",
      "updatedAt": "2026-05-14T16:24:09.123Z"
    }
  ]
}

GET /api/v1/monitors/{id}

Returns a single monitor. Responds with 404 if the monitor does not exist or belongs to a different organization.

GET /api/v1/monitors/{id}/checks

Most recent check results for a monitor (newest first).

Query paramTypeDescription
limitinteger (1–200)Default 50.

Each result includes the regional probe, the timing breakdown (DNS / TCP / TLS / TTFB / total), response metadata, certificate state, and ICMP packet loss.

FieldTypeDescription
idstringCheck result ID.
monitorIdstringID of the monitor this check belongs to.
regionstringProbe region that performed the check (for example us-east-1).
timestampstringISO 8601 UTC timestamp when the check ran.
dnsLookupTimenumberDNS resolution time, in milliseconds.
tcpConnectTimenumberTCP connection time, in milliseconds.
tlsHandshakeTimenumberTLS handshake time, in milliseconds. 0 for checks that never reach TLS.
ttfbnumberTime to first byte, in milliseconds.
totalDurationnumberTotal check duration, in milliseconds.
responseSizenumberResponse body size, in bytes.
statusCodenumber | nullHTTP status code, or null for non-HTTP checks and failures that never received a response.
isUpbooleanWhether this individual check passed.
errorTypestring | nullShort error classification, or null if the check succeeded. Current values: timeout, dns_failure (the hostname didn't resolve), connection_refused, connection_reset, tls_error, host_unreachable, network_error, redirect_error, ping_failed, tcp_failed, ssrf_blocked (the target resolved to a private or internal address — see Getting started), ssl_expiry_warning, ssl_chain_validation_failure, and ssl_tls_policy_violation. Historical only: dns_error — an earlier name for the same DNS-resolution failure, retired 2026-07-28 and no longer written by any check. Check results from before that date may still carry it (older rows aren't rewritten, so this is expected on historical data, not a bug); it means exactly what dns_failure means. Treat this as an open string — new values may be added without notice.
errorMessagestring | nullHuman-readable error detail, or null if the check succeeded.
sslExpiresAtstring | nullISO 8601 UTC expiry of the certificate presented during this check, or null. See Certificate fields below.
sslDaysUntilExpirynumber | nullInteger days until expiry, computed against the probe's clock at check time, or null. See Certificate fields below.
sslChainValidboolean | nullWhether the certificate chain validated, or null. See Certificate fields below.
sslChainErrorstring | nullValidation failure detail (an OpenSSL/Node TLS error code, for example CERT_HAS_EXPIRED or ERR_TLS_CERT_ALTNAME_INVALID, capped at 500 characters), or null. For display and logging — branch on sslChainValid, not this string. See Certificate fields below.
icmpPacketLossnumber | nullNon-negative integer percentage of ICMP echo replies lost, or null when loss wasn't measured. See Packet loss below.

Certificate fields

Four fields carry certificate state, and they only make sense read together — none of them is meaningful on its own:

sslChainValidsslExpiresAtMeaning
truetimestampCertificate valid. Use sslDaysUntilExpiry for runway.
falsenullValidation failed — expired, self-signed, untrusted, or a hostname mismatch. See sslChainError.
nullnullNo TLS handshake was attempted — a tcp or ping monitor, or a plain HTTP monitor.

The negative you'll never see: sslDaysUntilExpiry is an integer and may in principle be negative — the API doesn't clamp it. In practice, Pingara's probes validate certificates strictly, so an expired certificate fails the TLS handshake before any certificate data is read: sslExpiresAt and sslDaysUntilExpiry both become null, and sslChainValid becomes false. 0 — meaning "expires within 24 hours" — is therefore the last numeric reading you'll see before a certificate lapses.

  • Prefer sslExpiresAt over sslDaysUntilExpiry. The days figure is a Math.floor snapshot against the probe's clock at check time — a check from a day ago reporting 7 actually means 6 today. If you're building a renewal dashboard, recompute runway from the absolute timestamp.
  • On a monitor, the same field is a bigger trap. monitors[].sslDaysUntilExpiry is a snapshot as of lastCheckedAt, not now. A paused monitor keeps its last reading indefinitely, so it can still report 30 a year later. Compute live runway from the monitor's sslExpiresAt instead.
  • SSL expiry alerts are milestone-based, per certificate. You get one notification per threshold crossed (the monitor's sslExpiryWarningDays) for the certificate currently installed — not one per threshold forever. Milestones re-arm when the installed certificate's expiry timestamp changes: a renewal, a replacement, a re-issue, or a host change all count. A replacement certificate with an identical expiry does not re-arm — same expiry means the same milestones, so nothing is missed. During a multi-region certificate rotation, regions can briefly disagree about which certificate they see, so a milestone can repeat a small number of times until they converge. This is self-limiting — it ends once every region sees the new certificate — and it's reachable on any rotation where a region still observing the outgoing certificate sees it inside a configured threshold. Treat SSL expiry notifications as at-least-once, not exactly-once, and track state on your side if you need continuous coverage rather than threshold-crossing events.
  • A monitor already stuck silent under the older bug doesn't self-heal on upgrade. If a monitor went permanently quiet before this fix shipped, it stays quiet for the remainder of its currently installed certificate and resumes normal milestone alerts at that certificate's next renewal. There's no backfill — a correctly notified monitor and a stuck one are indistinguishable in the database.
  • Both monitor-level SSL fields are null until the monitor's next TLS check runs. There's no backfill for monitors that existed before this field shipped.

Same certificate data, a different shape: the webhook payload carries these values nested under ssl.expiresAt / ssl.daysUntilExpiry; the Developer API is flat (sslExpiresAt / sslDaysUntilExpiry). Same tokens, direct mechanical mapping — worth knowing before you write a client that talks to both.

Packet loss

icmpPacketLoss is telemetry from a 3-packet ICMP sample, not a status signal — it never affects isUp.

  • The sample is 3 packets. Every check sends exactly three echo requests, so a normal reading is one of four values: 0, ~33, ~67, or 100. There's no such thing as "5% loss" in a Pingara check result. The API doesn't clamp icmpPacketLoss to 100, so a value above that isn't a guarantee violation — it indicates a probe fault, and you should treat it as suspect rather than as a loss percentage.
  • Don't alert on a single check. One check reporting 33% loss is one dropped packet out of three — the 95% confidence interval for "1 of 3" runs roughly 0.8%–91%. On a path with a true 5% loss rate, 14.3% of individual checks will show at least one drop. At a 1-minute interval, paging on any nonzero reading produces on the order of 205 false-positive alerts per day, per region.
  • The right pattern. Require at least 3 consecutive checks with loss greater than 0 in the same region, and a multi-region quorum — at least half the monitor's regions and never fewer than two, the same quorum rule Pingara's own down/up detection uses, except for a single-region monitor, where that one region decides. Better still, aggregate instead of counting checks: every sample is exactly 3 packets, so a plain window mean is unbiased. A 15-minute window across 4 regions at a 1-minute interval is 180 packets — 0.56% resolution. A reasonable starting point: window mean ≥5% sustained across two consecutive windows in at least 2 regions opens a ticket, not a page. Short of 100% loss, this is degradation, and degradation alone doesn't justify waking someone up.
  • Don't compare with ===. GNU ping reports integers; Pingara's fallback probe computes floats. Both are rounded before serialization, but agreement between the two is only ±1. Compare with a range, or convert to packet counts: packetsLost = Math.round(icmpPacketLoss * 3 / 100) — this assumes an in-range (0–100) reading; an out-of-range value means a probe fault, not a packet count.
  • icmpPacketLoss ?? 0 is a bug in your integration. null means "not measured," not "no loss." ping monitors always set it. http monitors set it only when the probe has ICMP enabled and the hostname resolves to something pingable. tcp monitors never set it.
  • Treat loss as a precursor signal, cautiously. Sustained cross-region packet loss means something on the path is dropping packets, and that's worth investigating. It does not mean an outage is coming — most loss episodes never turn into one. Two caveats: routers commonly deprioritize and rate-limit ICMP, so low single-digit loss to an otherwise healthy host is normal and not worth chasing; and for http monitors, packet loss is pure telemetry that never affects isUp — if HTTP is clean and ICMP is lossy, believe HTTP.
  • How it's measured. Pingara measures loss with ICMP echo where the probe can send it, and falls back to an equivalent TCP-reachability probe where it can't. The field doesn't tell you which method produced a given reading.
  • This field doesn't move with latency. ttfb and totalDuration only average the packets that actually returned, so they don't rise with loss — and can trend downward as dropped attempts fall out of the average.
  • Nothing in Pingara's own alerting reads icmpPacketLoss for status today — the Developer API is currently the only way to see partial ping loss at all.

GET /api/v1/monitors/{id}/incidents

Recent incidents for a single monitor, newest first.

GET /api/v1/incidents

Recent incidents across the whole organization.

Query paramTypeDescription
limitinteger (1–200)Default 50.
statusstringFilter to investigating, identified, monitoring, or resolved.

GET /api/v1/incidents/{id}

Returns a single incident with start/resolve timestamps, the affected regions, error metadata, and the AI-generated root-cause hint (when available). Responds with 404 if the incident does not exist or belongs to a different organization.

FieldTypeDescription
idstringIncident ID.
monitorIdstringID of the monitor this incident belongs to.
statusstringOne of investigating, identified, monitoring, resolved.
startedAtstringISO 8601 UTC timestamp when the incident opened.
resolvedAtstring | nullISO 8601 UTC timestamp when the incident resolved, or null while open.
acknowledgedAtstring | nullISO 8601 UTC timestamp when a team member acknowledged the incident, or null if unacknowledged.
errorTypestring | nullShort error classification captured at incident open. Same value set as check results' errorType above (timeout, dns_failure, connection_refused, connection_reset, tls_error, host_unreachable, network_error, redirect_error, ping_failed, tcp_failed, ssrf_blocked, and the SSL-specific values), including the dns_failure-current / dns_error-historical-only distinction described there — an open string, treat unrecognized values as informational.
errorMessagestring | nullHuman-readable error detail captured at incident open.
rootCauseHintstring | nullAI-generated root-cause suggestion, or null if not yet generated.
affectedRegionsstring[]Regions that reported the failure. Empty array if none were recorded.
avgResponseTimenumber | nullAverage response time (ms) across affected checks, or null if unavailable.

Example response:

{
  "data": {
    "id": "i9zz…",
    "monitorId": "m1aa…",
    "status": "resolved",
    "startedAt": "2026-05-12T09:11:02.000Z",
    "resolvedAt": "2026-05-12T09:26:47.500Z",
    "acknowledgedAt": "2026-05-12T09:13:10.000Z",
    "errorType": "timeout",
    "errorMessage": "Request timed out after 10000ms",
    "rootCauseHint": "Upstream DNS latency spiked across two regions, consistent with a provider-side issue rather than an application error.",
    "affectedRegions": ["us-east-1", "eu-west-1"],
    "avgResponseTime": 9800
  }
}

4. Conventions

Response envelope

List endpoints return:

{ "data": [ ... ], "count": <number> }

Single-resource endpoints return:

{ "data": { ... } }

Timestamps

All timestamps are ISO 8601 strings in UTC, for example "2026-05-14T16:24:09.123Z".

Errors

Errors use standard HTTP status codes plus a JSON body:

StatusMeaning
400Bad request — invalid query parameter
401Missing, malformed, or revoked API key
404Resource not found (or not in your organization)
429Rate limit exceeded — see Retry-After header
5xxServer error — retry with exponential backoff

Rate limiting

Each API key may make up to 600 requests per minute. When you exceed the limit, the API returns 429 Too Many Requests with a Retry-After: 60 header. Build clients that respect both — a small fixed delay or exponential backoff works well.

CORS

The Developer API responds to cross-origin requests from any origin and supports preflight OPTIONS for the Authorization and Content-Type headers, so you can call it directly from browser-based dashboards if you're comfortable exposing the key (in general you should proxy through your backend instead).

5. Examples

curl: list every down monitor

curl -sS "https://api.pingara.io/api/v1/monitors?status=down" \
  -H "Authorization: Bearer $PINGARA_API_KEY" | jq '.data[].name'

Node.js / TypeScript

const base = process.env.PINGARA_API_BASE!; // https://api.pingara.io/api/v1
const key = process.env.PINGARA_API_KEY!;

async function listOpenIncidents() {
  const res = await fetch(`${base}/incidents?status=investigating&limit=100`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  if (!res.ok) throw new Error(`Pingara API ${res.status}: ${await res.text()}`);
  const body = (await res.json()) as { data: Array<{ id: string; monitorId: string; startedAt: string }> };
  return body.data;
}

Python

import os, requests

base = os.environ["PINGARA_API_BASE"]
key = os.environ["PINGARA_API_KEY"]

resp = requests.get(
    f"{base}/monitors",
    headers={"Authorization": f"Bearer {key}"},
    params={"limit": 200},
    timeout=15,
)
resp.raise_for_status()
for m in resp.json()["data"]:
    print(m["status"], m["name"], m["url"])

GitHub Actions deploy gate

- name: Block deploy if any monitor is down
  env:
    PINGARA_API_KEY: ${{ secrets.PINGARA_API_KEY }}
  run: |
    count=$(curl -sS \
      "$PINGARA_API_BASE/monitors?status=down" \
      -H "Authorization: Bearer $PINGARA_API_KEY" | jq '.count')
    if [ "$count" -gt 0 ]; then
      echo "::error::$count monitors currently down — aborting deploy"
      exit 1
    fi

6. Versioning & deprecation policy

  • The v1 surface is stable. We will not remove fields or change types without bumping the major version.
  • Additive changes (new fields on existing responses, new endpoints, new query parameters with safe defaults) ship under v1 and do not constitute a breaking change.
  • When v2 ships, v1 will continue to operate for at least 12 months after the v2 GA announcement.

7. What's next

Roadmap items we're tracking for upcoming releases:

  • Write endpoints (create / pause / resume monitors, acknowledge incidents)
  • Scoped keys (read-only vs. write-only, per-monitor scopes)
  • Webhook event subscriptions (push delivery of incident.opened, incident.resolved, monitor.status_changed)
  • OpenAPI 3.1 spec for code generation
  • SDKs for Node.js, Go, and Python

Have feedback or a use case we haven't covered? Email support@pingara.io — we'd love to hear from you.