PulseWatchPulsewatch

REST API (v1)

A small, key-authed REST API for managing monitors. It exists for provisioning fleets and wiring PulseWatch into your own tooling. Base URL: https://pulsewatcher.up.railway.app/api/v1.

Authentication

Create an API key in Settings and send it as a bearer token. Keys are shown once and stored hashed. Read endpoints work on any plan; write endpoints require Pro or Business (free keys are read-only).

http
Authorization: Bearer pw_live_…

Listing monitors with a key

curl -fsS "https://pulsewatcher.up.railway.app/api/v1/monitors" \
  -H "Authorization: Bearer $PW_API_KEY"

Endpoints

GET/api/v1/monitorsList your monitors. Query: state, tag.[read]
POST/api/v1/monitorsCreate a monitor. Body: { type, ... }.[write]
GET/api/v1/monitors/:idFetch one monitor.[read]
PATCH/api/v1/monitors/:idUpdate fields.[write]
DELETE/api/v1/monitors/:idSoft-delete a monitor.[write]
POST/api/v1/monitors/:id/pausePause a monitor.[write]
POST/api/v1/monitors/:id/resumeResume (state resets to new).[write]
GET/api/v1/monitors/:id/pingsRecent pings. Query: limit (1–100).[read]
GET/api/v1/monitors/:id/checksRecent uptime checks. Query: limit (1–100).[read]

Create a monitor

bash
curl -fsS -X POST "https://pulsewatcher.up.railway.app/api/v1/monitors" \
  -H "Authorization: Bearer pw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "uptime",
    "name": "Checkout API",
    "url": "https://api.example.com/healthz",
    "check_interval_seconds": 60,
    "tags": ["payments", "prod"]
  }'

201 response: the full monitor row

json
{
  "monitor": {
    "id": "b0000000-0000-4000-a000-000000000003",
    "user_id": "a0000000-0000-4000-a000-000000000001",
    "type": "uptime",
    "name": "Checkout API",
    "state": "up",
    "period_seconds": null,
    "grace_seconds": null,
    "last_ping_at": null,
    "ping_token": "9f2c1e7a-0000-4000-a000-0000000000a4",
    "url": "https://api.example.com/healthz",
    "check_interval_seconds": 60,
    "timeout_ms": 10000,
    "expected_status_min": 200,
    "expected_status_max": 299,
    "last_checked_at": "2026-07-21T14:03:00.000Z",
    "consecutive_failures": 0,
    "on_status_page": true,
    "created_at": "2026-07-01T09:00:00.000Z",
    "deleted_at": null,
    "alert_after_seconds": 0,
    "repeat_interval_seconds": 0,
    "down_since": null,
    "last_alert_at": null,
    "ssl_valid_to": "2026-09-30T00:00:00.000Z",
    "ssl_checked_at": "2026-07-21T00:00:00.000Z",
    "ssl_alerted_days": null,
    "expect_text": null,
    "forbid_text": null,
    "degraded_threshold_ms": null,
    "degraded": false,
    "schedule_kind": "period",
    "cron_expression": null,
    "cron_timezone": null,
    "next_expected_at": null,
    "last_start_at": null,
    "max_duration_seconds": null,
    "tags": [
      "payments",
      "prod"
    ],
    "show_latency_on_status": true,
    "alert_on_new_error": true,
    "escalate_after_consecutive": null,
    "last_error_alert_at": null,
    "autoprovisioned": false,
    "autoprovision_slug": null,
    "schedule_learning": false
  }
}

Heartbeat monitors send period_seconds and grace_seconds instead of url/check_interval_seconds. Pro-only fields (cron schedules, keyword checks, degraded thresholds) return 402 pro_feature on Free.

Reading pings & checks

GET /monitors/:id/pings → { pings: [...] }

json
{
  "pings": [
    {
      "id": 84210,
      "monitor_id": "b0000000-0000-4000-a000-000000000003",
      "received_at": "2026-07-21T03:00:12.412Z",
      "kind": "success",
      "source_ip": "203.0.113.7",
      "user_agent": "curl/8.6.0",
      "duration_ms": 4120,
      "metrics": {
        "rows": 48210,
        "errors": 0
      },
      "body_excerpt": "backup complete: 48210 rows in 4.1s",
      "stderr_truncated": false
    }
  ]
}

GET /monitors/:id/checks → { checks: [...] }

json
{
  "checks": [
    {
      "id": 553120,
      "monitor_id": "b0000000-0000-4000-a000-000000000003",
      "checked_at": "2026-07-21T14:03:00.000Z",
      "ok": true,
      "status_code": 200,
      "latency_ms": 142,
      "error": null
    }
  ]
}

Both accept ?limit= (1–100, default 25), newest first. The monitors list returns all your non-deleted monitors and is not paginated.

Errors & rate limits

Errors are JSON: { "error": "…", "code": "…" }. Requests are limited to 60 per minute per key; over the limit returns 429 with a Retry-After header.

HTTPcodeWhen
400Malformed body or invalid field (issues[] included).
401Missing or invalid API key.
402monitor_limitPlan monitor cap reached.
402min_intervalCheck interval below your plan minimum.
402pro_featureA field requires the Pro or Business plan.
402status_page_planStatus pages require a paid plan.
403Free key attempted a write (writes are Pro+).
404Monitor not found (or not yours).
422invalidRejected by a database constraint.
429Rate limit exceeded (60 req/min per key).

Quickstart: provision a fleet

The multi-site story: loop over your hosts and create one uptime monitor each, tagged so you can filter the whole fleet in the dashboard.

# provision the same uptime check across many sites
KEY="pw_live_…"
for host in $(cat sites.txt); do
  curl -fsS -X POST "https://pulsewatcher.up.railway.app/api/v1/monitors" \
    -H "Authorization: Bearer $KEY" \
    -H "Content-Type: application/json" \
    -d "{\"type\":\"uptime\",\"name\":\"$host\",\"url\":\"https://$host/healthz\",\"check_interval_seconds\":60,\"tags\":[\"fleet\"]}"
done