API reference
Send heartbeats, update your status and schedule, post incidents.
The base URL is https://api.is-up.to. Send JSON with Content-Type: application/json and authenticate with Authorization: Bearer <token>.
| Token | Starts with | Can do |
|---|---|---|
| Device token | iut_d_ | Heartbeats for its device, status, schedule |
| Personal token | iut_p_ | Everything on your page, including services and incidents |
Errors return a JSON body like { "error": "invalid_request", "message": "…" }. Requests are rate limited per token; a 429 response includes a Retry-After header.
Devices
Device tokens belong to one device. Send a heartbeat every 60 seconds while the device is in use; if none arrives for the device's timeout (5 minutes by default), it shows as offline.
POST/v1/heartbeat
Device token
Report that this device is on, and what it's doing. Fields you've hidden for the device are accepted but never shown.
Request body
{
"state": "active", // "active" | "idle" | "locked"
"activity": {
"app": "Figma", // optional, max 80 chars
"title": "Checkout v2" // optional, max 160 chars
},
"battery": { "level": 82, "charging": false },
"message": "Rendering…" // optional, max 140 chars
}Response
{ "ok": true, "device": { "id": "dev_…", "name": "MacBook Pro" }, "interval": 60 }POST/v1/heartbeat/offline
Device token
Mark the device offline straight away, for example when it shuts down or the user signs out.
Response
{ "ok": true }Linking a device
Apps get a device token without the user typing a password, using the device authorization flow from RFC 8628. Show the user_code, send the user to verification_uri, and poll for the token.
POST/v1/device-codes
No token
Start linking. Codes expire after 10 minutes.
Request body
{ "name": "MacBook Pro", "kind": "laptop", "platform": "macos" }Response
{
"device_code": "…",
"user_code": "WDJB-MJHT",
"verification_uri": "https://portal.is-up.to/link",
"verification_uri_complete": "https://portal.is-up.to/link?code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}POST/v1/device-codes/token
No token
Poll no faster than interval. Returns 400 with "authorization_pending" until the user approves, "slow_down" if polling too fast, and "expired_token" or "access_denied" when it's over.
Request body
{ "device_code": "…" }Response
{
"token": "iut_d_…",
"device": { "id": "dev_…", "name": "MacBook Pro" },
"page": { "username": "alex", "url": "https://alex.is-up.to" }
}Status
Your status is the line under your name. An until time clears it automatically.
GET/v1/presence
Device or personal token
Read your current status.
Response
{ "state": "busy", "text": "Heads down", "emoji": null, "until": "2026-09-29T15:00:00Z" }PUT/v1/presence
Device or personal token
Replace your status. Send state "auto" to let device activity decide.
Request body
{
"state": "busy", // "auto" | "available" | "busy" | "away" | "dnd" | "sleeping" | "offline"
"text": "Heads down",
"emoji": "🎧",
"until": "2026-09-29T15:00:00Z"
}Response
{ "state": "busy", "text": "Heads down", "emoji": "🎧", "until": "2026-09-29T15:00:00Z" }Schedule
Times are ISO 8601 with an offset or Z. Private events appear on your page as busy blocks without a title.
GET/v1/events?from=…&to=…
Device or personal token
List events that overlap a range. The range can be at most 62 days.
Response
{ "events": [ { "id": "evt_…", "title": "Design review", "starts_at": "…", "ends_at": "…", "kind": "meeting", "visibility": "public" } ] }POST/v1/events
Device or personal token
Create an event.
Request body
{
"title": "Design review",
"starts_at": "2026-09-29T15:00:00+01:00",
"ends_at": "2026-09-29T16:00:00+01:00",
"kind": "meeting", // "busy" | "meeting" | "focus" | "travel" | "sleep" | "away" | "free"
"visibility": "public", // "public" | "private"
"location": "Zoom",
"notes": "Optional"
}Response
{ "id": "evt_…", … }PATCH/v1/events/{id}
Device or personal token
Update any fields of an event you created.
Response
{ "id": "evt_…", … }DELETE/v1/events/{id}
Device or personal token
Delete an event.
Response
{ "ok": true }Services and incidents
Personal tokens can do everything you can do in the portal. Use them from CI to open incidents during deploys.
GET/v1/services
Personal token
List services with their current status.
Response
{ "services": [ { "id": "svc_…", "name": "API", "status": "operational" } ] }PATCH/v1/services/{id}
Personal token
Set a service's status by hand. Monitored services go back to automatic after the next successful check.
Request body
{ "status": "degraded" } // "operational" | "degraded" | "partial_outage" | "major_outage" | "maintenance"Response
{ "id": "svc_…", "status": "degraded" }POST/v1/incidents
Personal token
Open an incident with its first update.
Request body
{
"title": "Elevated API errors",
"status": "investigating", // "investigating" | "identified" | "monitoring" | "resolved"
"impact": "major", // "none" | "minor" | "major" | "critical"
"message": "Some requests are failing with 502 errors.",
"services": ["svc_…"]
}Response
{ "id": "inc_…", … }POST/v1/incidents/{id}/updates
Personal token
Post an update. Setting status to resolved closes the incident.
Request body
{ "status": "resolved", "message": "Error rates are back to normal." }Response
{ "id": "upd_…", … }Public data
These endpoints need no token and are cached for up to 15 seconds.
GET/v1/pages/{username}
No token
Everything shown on a page, as JSON. Hidden fields and private event titles are never included.
Response
{ "username": "alex", "presence": { … }, "services": [ … ], "devices": [ … ], "events": [ … ] }GET/v1/usernames/{username}
No token
Check whether a username can be registered.
Response
{ "username": "alex", "available": false, "reason": "That username is taken." }Looking for something that isn't here? Tell us what you're building.