Upwatch REST API
Manage monitors, incidents, maintenance windows, status pages and alert integrations programmatically. JSON in, JSON out.
| Base URL | /api/v1 |
|---|---|
| Format | JSON request and response bodies (Content-Type: application/json) |
| IDs | Numeric, e.g. monitor 800000001 |
| Dates | ISO 8601 UTC, e.g. 2026-11-01T14:30:00.000Z |
| CORS | Enabled — the API can be called from browsers. |
Authentication
Create keys in the dashboard under Settings → API, then send the key in the Authorization header:
| Key type | Prefix | Access |
|---|---|---|
| Main | uw_main_… | Full read & write access to every endpoint. |
| Read-only | uw_read_… | GET requests only. Ideal for dashboards (Grafana, Home Assistant). |
| Monitor-specific | uw_mon<id>_… | GET on one monitor only: /monitors, /monitors/{id}, its stats and logs. |
| App token | uwa_… | Issued to the upWatch mobile app at sign-in (see Mobile app API). Works on every plan: all GET endpoints plus POST /monitors/{id}/pause|start|check|reset and POST /incidents/{id}/comments. Anything else returns 403-005. |
Missing or invalid keys get 401; a read-only or monitor key used for writing gets 403.
Rate limits
600 requests per minute per key. Every response carries:
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests left in the current minute |
X-RateLimit-Reset | Unix time (seconds) when the window resets |
Retry-After | Seconds to wait — only on 429 responses |
Pagination
List endpoints accept limit (1–200, default 50) and cursor. They return:
{
"data": [ … ],
"nextLink": "https://host/api/v1/monitors?limit=50&cursor=50" // null on the last page
}
Follow nextLink until it is null.
Errors
Errors use standard HTTP status codes and a consistent body:
{
"code": "404-001",
"message": "Monitor not found",
"statusCode": 404,
"error": "Not Found"
}
| Status | Meaning |
|---|---|
| 400 | Validation failed — message says which field is wrong |
| 401 | Missing / invalid API key |
| 403 | Key not allowed to do this |
| 404 | Resource or endpoint not found |
| 405 | Method not allowed on this path |
| 429 | Rate limit exceeded |
| 500 | Server error |
Types & statuses
Monitor types
| type | Checks | Key fields |
|---|---|---|
HTTP | Website / endpoint responds with an "up" status code | url, httpMethodType, successHttpResponseCodes, customHttpHeaders, postValueData, httpUsername/httpPassword, checkSSLErrors, followRedirections |
KEYWORD | Page contains / does not contain text | + keywordValue, keywordType, keywordCaseType |
API | Assertions on JSON body, headers, status or text | + assertions |
PING | ICMP echo | url (host / IP) |
PORT | TCP port accepts connections | url (host), port |
UDP | UDP service answers | url (host), port, udp |
DNS | DNS record resolves (to an expected value) | url (hostname), dnsRecordType, dnsExpectedValue, dnsServer |
SSL | Certificate valid and not expiring soon | url (host), port, sslExpirationDays |
DOMAIN | Domain registration not expiring soon (WHOIS) | url (domain), domainExpirationDays |
HEARTBEAT | Your job calls a URL regularly | interval, gracePeriod — the ping URL is returned in url |
Monitor statuses
STARTED | Created or resumed, waiting for the first result |
UP | Last check passed |
LOOKS_DOWN | Check failed but not yet confirmed (see confirmationChecks) — no alert yet |
DOWN | Confirmed down — incident open, alerts sent |
PAUSED | Monitoring paused |
MAINTENANCE | Inside a maintenance window — not checked, no alerts |
Other values
keywordType | ALERT_EXISTS (alert when found) · ALERT_NOT_EXISTS (alert when missing) |
keywordCaseType | CaseSensitive · CaseInsensitive |
httpMethodType | GET HEAD POST PUT PATCH DELETE OPTIONS |
postValueType | RAW_JSON · KEY_VALUE (form-encoded) |
assertion source | body_json (selector like $.data[0].ok) · header (selector = header name) · status_code · body_text |
assertion comparison | equals not_equals contains not_contains greater_than less_than exists not_exists matches_regex |
maintenance interval | once daily weekly monthly; days = 1–7 (Mon–Sun) for weekly, 1–31 or -1 (last day) for monthly |
status page sort | custom az za status_down status_up |
integration type | Email Telegram Slack Discord MSTeams GoogleChat Mattermost Pushover PushBullet Pagerduty Zapier Webhook |
incident status | OPEN · RESOLVED |
Heartbeat URLs
A HEARTBEAT monitor's url is its ping URL. Call it (any method, no API key) from your job on every run. If no call arrives within interval + gracePeriod, the monitor goes DOWN.
curl -fsS https://host/api/heartbeat/<token> # Linux cron
Invoke-WebRequest https://host/api/heartbeat/<token> # Windows
/tool fetch url="https://host/api/heartbeat/<token>" keep-result=no # MikroTik
What each check stores
| Type | details fields |
|---|---|
| HTTP / KEYWORD / API | request, ip, status, statusText, httpVersion, timing {dns, connect, tls, firstByte, total}, headers, sizeBytes, contentType, redirects [{url, status, ms}], finalUrl, tls {protocol, cipher}, certificate {subject, issuer, validFrom, validTo, daysLeft, altNames, serial, fingerprint256}, keyword {found, position, context}, assertions {results [{…, actual, passed}]}, bodySnippet (on failure), error |
| PING | host, ip, ttl, roundTripMs, output |
| PORT | host, port, ip, connectMs, banner |
| UDP | host, port, sentBytes, response {from, bytes, text, hex} |
| DNS | host, recordType, resolver, resolverServers, records [{value, ttl}], expected, missing |
| SSL | host, port, ip, protocol, cipher, trusted, trustError, certificate {…}, chain [{subject, validTo}] |
| DOMAIN | domain, whoisServer, registrar, created, updated, expires, daysLeft, nameServers, statuses, raw |
| HEARTBEAT | expectedEverySeconds, graceSeconds, lastBeat, secondsSinceLastBeat, lastBeatFrom {ip, method, userAgent, message} |
Heartbeats can carry a message: …/api/heartbeat/<token>?msg=backup+ok+12GB or POST a text / JSON {"message": "…"} body. It is stored with each check.
Webhook payload
Webhook and Zapier integrations receive a JSON POST for each event (down, up, ssl, storm, test):
{
"event": "down",
"time": "2026-11-01T14:30:00.000Z",
"monitor": { "id": 800000001, "name": "Main website", "type": "http", "target": "https://example.com", "status": "down" },
"reason": "HTTP 503 Service Unavailable",
"durationSeconds": null, // set on "up": how long it was down
"incidentId": 42,
"subject": "🔴 Monitor is DOWN: Main website",
"message": "🔴 Monitor is DOWN: Main website\nTarget: …"
}
Mobile app API
Used by the upWatch Android app. Base URL /api/mobile/v1, JSON in and out, same error format as above. Every endpoint except /info, /auth/login and /auth/pair needs Authorization: Bearer uwa_…. App tokens belong to one user and workspace, can be signed out from the web under Account → Mobile app, and keep working whatever plan the workspace is on.
| Endpoint | Description |
|---|---|
GET /info | Public. { appName, logoUrl, version, apiVersion, signupUrl, webUrl, pushProvider: "builtin" | "fcm", push: { stream: true, unifiedPush: true, fcm } }. Use it to validate a server URL. |
POST /auth/login | { email, password, deviceName } → { token, tokenId, user, workspace, workspaces }. Limited to 10 attempts per 5 minutes per IP. |
POST /auth/pair | { code, deviceName }, the code from web Account → Connect Mobile App (format XXXX-XXXX, 10 minutes, single use; the QR encodes upwatch://pair?server=…&code=…). Same response as login. |
POST /auth/logout | Revokes this token and unregisters its devices. 204. |
GET /me | User, workspace (plan, status, trial/period end), limits, push provider and this sign-in's devices. |
GET /devices | Devices registered with this token. |
POST /devices | { installId, unifiedPushEndpoint?, fcmToken?, platform: "android", name, appVersion, locale, timezone, prefs? } → 201 { deviceId, transports, … }. installId is a random id the app generates once and keeps; re-registering with it updates the same device. unifiedPushEndpoint (optional) is the URL from the phone's UnifiedPush distributor; loopback and link-local addresses are refused. fcmToken (optional) is only used if the server has Firebase configured. The first device creates the workspace's upWatch app integration, which applies to every monitor. |
PATCH /devices/{id} | { prefs, fcmToken, name, appVersion, locale, timezone }. prefs = { events: { down, up, ssl, domain, comments }, critical, quietHours: { start: "22:00", end: "07:00" } | null, mutedMonitors: { "<monitorId>": untilEpochMs | null } }; partial updates merge; a mute set to false removes it, null = forever. |
DELETE /devices/{id} | Unregister. 204. |
POST /devices/{id}/test | Sends a test push to that device. |
GET /stream?deviceId= | Built-in live delivery, no Google needed. A Server-Sent Events stream. It sends event: hello on connect, then event: alert with id: <cursor> and the push payload as data, plus a : ping comment every 25 s. Reconnect with the Last-Event-ID header (or ?since=) to replay what was missed. With deviceId, every alert carries notify ("true"/"false", after this phone's preferences) and critical; alerts with notify = "false" still belong in the inbox. Auth: Authorization: Bearer, or ?token= for clients that can't set headers. Up to 3 streams per sign-in. |
GET /events?since=&limit= | Alert feed (same objects as the push data), oldest first. { data, cursor, hasMore }; pass the returned cursor as since next time. Events are recorded even when a push is muted or push is not configured; kept 30 days / 1,000 per workspace. |
POST /incidents/{id}/ack | { note? }: adds "✅ Acknowledged by <name> from upWatch" to the incident. 201. |
For monitors, stats, checks, incidents and maintenance the app calls the regular /api/v1 endpoints with its app token.
Push payload
The same payload is delivered over every transport the phone has:
- Live stream (
GET /stream): built in, needs no setup, and works on phones without Google services. - UnifiedPush: JSON
POSTto the phone's distributor endpoint (for example a self-hosted ntfy server), withUrgency: highfor down alerts. A404/410response removes the endpoint. - Firebase (optional): data-only FCM HTTP v1 with
android.priority = HIGH, used only when an admin uploads a service account.UNREGISTEREDtokens are removed.
A phone holding a live stream gets the alert there and isn't pushed twice. All values are strings. The server applies each device's preferences: event types, muted monitors, and quiet hours in the device's timezone. During quiet hours only down/storm get through, and only with critical enabled.
{
"v": "1",
"type": "down", // down | up | ssl | domain | comment | storm | test
"eventId": "evt_1287",
"workspaceId": "1001", "workspaceName": "Alphabridge Technologies",
"monitorId": "42", "monitorName": "Mariah Camp RADIUS", "monitorType": "PORT", "target": "10.0.0.5:1812",
"incidentId": "317",
"reason": "Connection refused",
"duration": "754", // up: seconds it was down
"days": "9", // ssl / domain: days left
"count": "6", // storm: monitors down
"time": "2026-11-02T01:40:12.000Z",
"url": "https://status.example.com/app#/incident/…",
"title": "🔴 Monitor is DOWN: Mariah Camp RADIUS",
"body": "🔴 Monitor is DOWN: …",
"critical": "false" // true when this device opted in to critical down alerts
}
Upwatch API v1 · self-hosted