Errors & rate limits

What the API returns when things go wrong, and what to expect under throttling or backend pressure.

Error envelope

All 4xx and 5xx responses share the same shape: a JSON body with a single error field.

json
{ "error": "Rate limit exceeded. Maximum 10 pings per minute." }

Status codes

FieldTypeDescription
200OKRequest succeeded. Inspect the response body for the structured result.
400Bad requestValidation failed — required field missing, invalid enum, or payload size exceeded.
401UnauthorizedMissing or invalid X-Webhook-Signature.
403ForbiddenTeam subscription is inactive or expired.
404Not foundUnknown ping URL or webhook URL, or the resource is paused/disabled.
429Too many requestsRate limit or monthly usage quota exceeded. The body explains which.
5xxServer errorUnexpected failure on our end. Retry with backoff; if persistent, contact support.

Rate limits

  • Pings — 10 requests per minute per ping URL. Bursting above that returns 429 for the rest of the minute.
  • Alerts — limited primarily by the webhook's configured rate cap and the team monthly quota. Identical alerts are deduplicated server-side, so high-volume failure modes don't immediately hit the limit.

Monthly quotas

Each plan includes a monthly cap on heartbeats and alerts. When the cap is exhausted, further requests return 429 with a reason like "Monthly heartbeat quota exceeded". Usage resets at the start of each billing period; usage and cap are visible in Settings → Usage.

Retry guidance

  • 401 / 403 / 404 — do not retry. The request is fundamentally rejected; fix the cause.
  • 429 — retry with exponential backoff. The SDKs do not retry automatically, so add retry logic if your workload warrants it.
  • 5xx — retry with exponential backoff, capped at a few attempts. Beyond that, surface the failure to your logs.

Errors from the SDKs

Both SDKs surface failures as OpShiftError with statusCode / status_code. Network failures (DNS, connection refused, timeout) surface with code 0 so you can distinguish "couldn't reach the API" from "the API said no".

typescript
try {
  await opshift.alert("deploy-failures", { title: "..." });
} catch (error) {
  if (error instanceof OpShiftError) {
    if (error.statusCode === 0) {
      // network-level failure: queue locally, retry later
    } else if (error.statusCode === 429) {
      // back off
    } else {
      // unrecoverable: surface to your log pipeline
    }
  }
  throw error;
}