This article covers what the Developer API returns when a request fails or is refused, how its rate limits and per-resource brakes work, and how to retry a write safely with an Idempotency-Key. The endpoints themselves are in the Developer API reference, and scopes are in API Keys and Scopes.
Errors
Errors use standard HTTP status codes, and every error body has the same shape:
{
"error": "Monitor not found",
"code": "not_found",
"retryable": false,
"requestId": "6f0c2a4e-1b7d-4c55-9e2f-3a8d0b1c7e94"
}
| Field | Meaning |
|---|---|
error | A human-readable sentence. Show it to a person, but don't branch on it, since the wording can change |
code | A stable, machine-readable reason, from the table below. Branch on this. Each code always comes with the same HTTP status |
retryable | true when sending the identical request again can succeed without anything changing on your side |
requestId | The id of this request. Quote it if you contact support |
details | Present only on some codes, with any of field, maxLength, requiredScope, retryAfterSeconds and planLimit. It never repeats a value you sent |
Every response from an API operation, success or error, also carries an
X-Request-Id header with the same value as requestId. Pingara makes
the id itself and ignores any X-Request-Id you send.
| Code | Status | Retryable | Meaning |
|---|---|---|---|
invalid_request | 400 | No | A malformed query parameter or body |
invalid_field | 400 | No | One field is invalid, such as a text field over its length limit. details.field names it, and details.maxLength gives the limit where there is one |
invalid_cursor | 400 | No | A pagination cursor that isn't one this API returned |
invalid_idempotency_key | 400 | No | An Idempotency-Key header that is empty or outside the allowed length or characters |
unauthorized | 401 | No | Missing, malformed, revoked or expired API key, or, on a read, a key whose creator is no longer a member of the organization |
insufficient_scope | 403 | No | The key lacks the scope this endpoint requires, see Key scopes. details.requiredScope names it |
scope_not_in_plan | 403 | No | The key holds the scope, but the organization's current plan does not allow it. details.requiredScope names it |
role_forbidden | 403 | No | The key's creator doesn't have the role this action needs, such as a viewer's key deleting a monitor, or (on GET /api/v1/audit-logs) a creator who is no longer an owner or admin |
key_creator_not_member | 403 | No | On a write, the key's creator is no longer a member of the organization |
key_creator_unverified | 403 | No | On a write, the key's creator has not verified their email address |
not_found | 404 | No | The resource doesn't exist, or isn't in your organization |
conflict | 409 | No | The request conflicts with current state, such as an address the monitor may not point at |
plan_limit_reached | 409 | No | A limit of your organization's plan, such as its monitor count. details.planLimit names the limit |
incident_resolved | 409 | No | Acknowledging an incident that is already resolved |
resolve_refused_monitor_down | 409 | No | Resolving an incident while its monitor is still down |
resolve_refused_monitor_degraded | 409 | No | Resolving an incident while its monitor is still degraded |
idempotency_in_progress | 409 | Yes | The original request with this Idempotency-Key is still running |
idempotency_response_unavailable | 409 | No | This Idempotency-Key was used and its original request was performed, but its response can't be replayed. Don't retry it |
cursor_unavailable | 409 | Yes | Cursor pagination on this endpoint is briefly unavailable. Retry without the cursor |
idempotency_key_reused | 422 | No | This Idempotency-Key was used with a different request |
rate_limited | 429 | Yes | A request rate limit, see Rate limiting |
rate_brake | 429 | Yes | A per-resource brake, see Per-resource brakes |
internal | 500 | On a GET only | An unexpected server error |
feature_disabled | 503 | Yes | The feature has been switched off on the platform, such as webhook subscriptions |
service_unavailable | 503 | Yes | Writes, or the feature, are temporarily unavailable. Reads are unaffected |
A 429 carries details.retryAfterSeconds with the same value as its
Retry-After header.
A 500 on a write is never marked retryable, even when you sent an
Idempotency-Key: the write may already have taken effect. Check, for
example by listing your monitors, before sending it again. A 500 on a
GET is safe to retry with backoff.
The Public Status Page API uses
the same error shape and the same X-Request-Id header.
Rate limiting
There are two independent limits on the transport, and a request has to clear both. Some write endpoints add a third kind of limit, on the resource itself, see Per-resource brakes below.
| Limit | Applies to | Ceiling |
|---|---|---|
| Per key, Free plan | Each API key in an organization on Free | 60 requests per minute |
| Per key, Pro plan | Each API key in an organization on Pro | 600 requests per minute |
| Per client IP | The calling IP address, checked before the key is looked up | 1,200 requests per minute |
The per-key limit follows your organization's plan. Organizations with a
Lifetime or Comped plan get the Pro limit. Reads and writes share one per-key
budget, so a key that polls heavily has less room to write. The limit is read
from your plan on every request: upgrade, and the next request gets 600;
downgrade, and the next request gets 60. A Free key that makes more than 60
requests in a minute gets 429 responses until its oldest requests are more
than 60 seconds old.
The per-IP limit exists to make guessing at API keys expensive. It is normally invisible, but it does bind on shared infrastructure: a CI runner, a NAT gateway, or one host driving several keys can reach 1,200 while no individual key is anywhere near its own limit.
Rate-limit headers
Every response to a successfully authenticated request carries the key's current
budget, not only a 429:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The per-key ceiling for your plan: 60 on Free, 600 on Pro |
X-RateLimit-Remaining | Requests left in the current window. This is what the next request will actually find. The request you just made is already counted |
For example, on a Pro key:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 587
Three things to know about them.
They describe the per-key limit only. There is no header for the per-IP limit, deliberately. Publishing the remaining budget of a limit whose purpose is to slow down callers who don't hold a valid key would tell an attacker exactly how much room they have left.
They appear only once a key has authenticated. A 401 carries none, and
neither does a 429 raised by the per-IP limit. Read together with
X-RateLimit-Remaining, that tells you which 429 you hit:
- No
X-RateLimit-*headers at all - the per-IP limit. Look at what else is calling from the same address, not at your own request rate. X-RateLimit-Remaining: 0- your key's own per-minute limit for your plan (60 on Free, 600 on Pro).X-RateLimit-*present with budget still on it - a per-resource brake. Your request rate is fine; the particular thing you were creating is what is capped, andRetry-Aftercan be far longer than a minute.
There is no X-RateLimit-Reset. Pingara's limiter is a sliding window:
each request expires 60 seconds after it was made, individually, so there is no
single moment at which your quota "resets" and no honest value to put in that
header. Use Retry-After instead.
Per-resource brakes
A few write endpoints carry a further limit of their own, on the resource rather than on the transport. Today there is exactly one: creating monitors is capped at 100 per hour, per organization, and separately at 300 per hour per human across every organization they belong to. Since an API key belongs to exactly one organization, the org limit, 100/hour, is the one a single key can actually reach. It exists to bound how fast rows can accumulate, not to pace your requests. In practice it's rarely the binding constraint: every create through this API pays your plan's monitor-count check the same way the UI does (1 on Free, 50 on Pro), so a Free-plan key can never get anywhere near 100 in the first place, and a Pro-plan key hitting 100 creates in an hour needs roughly 50 deletions inside that same hour to keep making room. The plan cap bounds how many monitors you can have; this brake bounds how fast you can create and delete them.
A request refused by one of these is also a 429, and it is still safe to
retry. The identical request succeeds once the window rolls, with nothing for
you to change. Two things make it different from the limits above: your key's
own budget is usually untouched, so X-RateLimit-Remaining still shows room;
and the wait is measured in minutes or hours rather than seconds. Read
Retry-After for the wait and the message in the body for what was capped.
Do not confuse this with a 409. A 409 from a write endpoint is a
refusal that will not clear on its own, such as a plan limit or a resource whose
current state forbids the change, and retrying it unchanged will be refused
again.
Retry-After
Every 429 carries Retry-After, in seconds, and it is computed rather
than fixed.
For the two transport limits it is the time until your oldest counted request falls out of the sliding window, which is the earliest moment a slot actually frees, and it is never more than 60. Waiting that long and retrying once is the correct behavior; a fixed sleep of a full minute is safe but usually longer than necessary.
For a per-resource brake it is that brake's own window instead, so it can be up to an hour. Treat it the same way, since it is still the answer to "when may I retry", but do not assume a ceiling of 60 seconds.
Build clients that respect Retry-After and back off further if a retry is
also rejected.
Idempotency-Key
Every write endpoint accepts an optional Idempotency-Key header: 8–255
characters from A-Za-z0-9._:+/=- (UUIDs, ULIDs, nanoids, and base64/base64url
all fit). Send the same key on a retry of the same request and you get back
the original response rather than a second write, for up to one hour.
curl -X POST https://api.pingara.io/api/v1/monitors \
-H "Authorization: Bearer $PINGARA_API_KEY" \
-H "Idempotency-Key: create-marketing-site-2026-05-14" \
-H "Content-Type: application/json" \
-d '{ "name": "Marketing site", "url": "https://www.example.com", "method": "GET", "environment": "production", "keywordCheckEnabled": false, "followRedirects": true, "maxRedirects": 5 }'
Creating a monitor is the genuinely unsafe write to retry without one. A
network failure after Pingara processed your request but before you saw the
response leaves you not knowing whether it succeeded, and blindly retrying a
bare POST /monitors makes a second monitor, silently, with no error to tell
you it happened. On the other writes an unkeyed retry is comparatively cheap:
acknowledging an incident twice is a no-op (see POST /api/v1/incidents/{id}/acknowledge), and pausing, resuming,
updating, or deleting something that's already in that state is either a
no-op or a 404/409 you'll notice. If a create returns a 5xx, don't
retry blind, even with an idempotency key: the key is released when a write
fails, so a retry can make a second monitor (see Errors). List your
monitors and check whether the one you meant to create is already there before
sending it again.
Next steps
- Developer API - Every endpoint, with request and response shapes
- API Keys and Scopes - Create a key and choose its scopes
Related Articles
Developer API
Create and manage monitors, acknowledge incidents, and read uptime, checks, status page state, and the audit log from Pingara programmatically using the v1 REST Developer API.
API Keys and Scopes
Create a Pingara Developer API key, understand its scopes and its expiry, rotate it without an outage, and understand why a request returns 401 or 403.