---
title: "Subscribe to the live event stream (SSE)"
method: GET
path: "/api/v1/events/stream"
tags: ["Events"]
---

# Subscribe to the live event stream (SSE)

`GET /api/v1/events/stream`

## Request

- Base URL: `https://api.uptimemonitoring.com`
- URL: `https://api.uptimemonitoring.com/api/v1/events/stream`
- Auth: none required

Consumes a ticket from `POST /api/v1/events/ticket` and upgrades the
HTTP response to an SSE stream (`text/event-stream`). Each frame is a
JSON envelope with `type`, `monitor_id`, `occurred_at`, and a
type-specific `data` payload. See `docs/SSE_LIVE_UPDATES_SPEC.md` for
the four event types and their payloads. Heartbeat comments
(`: heartbeat`) are emitted every 25 seconds.

**Credential re-checked at open AND periodically while connected
(issue #339).** A ticket's bound credential — the account, the
minting user, and the API key if one was presented — is re-verified
against durable storage both when the ticket is redeemed and, once
the stream is open, every ~30 seconds thereafter. Removing the
minting user from the account, revoking the API key past its grace
window, or suspending the account closes an already-open stream
within roughly 45 seconds on a healthy connection (a 30s recheck
interval + 5s check timeout detects the revocation and cancels the
stream in ~35s, plus up to a further 10s write deadline for an
in-flight write/flush to unblock and the stream to actually close)
— not instantly, and not a hard worst case: the write deadline is
armed only for the duration of a write+flush and cleared between
frames, so it bounds the handler's own blocking, not a stalled
HTTP/2 connection writer. A transient failure (e.g. a database
hiccup) never closes an open stream on its own; it only delays how
soon a genuine revocation takes effect.

## Query parameters

- `ticket` string, required

## Response `200`

SSE stream opened.

## Other responses

- `401`: Missing, invalid, expired, or already-consumed ticket — or a ticket whose bound API key has been revoked. A ticket refused here for a revoked key is also consumed (burned): the same ticket value cannot be retried even within its remaining TTL.
- `403`: The ticket's bound user is no longer a member of the account, or the account is suspended. Also consumes (burns) the ticket — it cannot be retried.
- `429`: Max concurrent SSE connections per credential reached (10 per credential — an API key, or a user for session/OAuth-JWT auth). This is a connection-cap rejection, not a rate-window rejection, so `Retry-After` is intentionally **not** set: retry depends on a peer disconnecting (no server-side clock event), and a guessed value would mislead clients into tight retry loops. Each API key has its own independent 10-slot budget shared across every caller who presents it; each session/OAuth-JWT user has their own independent 10-slot budget on the account, separate from every other user on that account. **This budget is shared with MCP GET `/mcp` streams** — REST event streams and MCP streams draw from the same per-credential counter, so a credential with zero open MCP streams can still hit this 429 if its REST streams (or vice versa) hold every slot. This 429's body text is shared byte-for-byte with the MCP refusal. This route is mounted outside the `/api/v1/*` Bearer block, so the dispatcher-wide 200/min limiter (`components.responses.RateLimited`) does not apply here — the connection cap is the only 429 source. A 429 here does NOT consume the ticket — retry with the same one once a slot frees.

---

[API](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api.md) · [All operations](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api/llms.txt) · [OpenAPI document](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api/revisions/ddcb2a8bc70f?raw)
