Skip to content
Docs/Events

Incidents & alerts

Events

Everything that happens in a workspace is recorded as an event. Events are what channels, subscribers and webhooks deliver, and agents can read the same stream by polling.

Event types

TypeWhendataDefault channelSubscribers
monitor.downA monitor went down (after confirmation).monitor, check, incident {id,title,url}, ai (opening report or null)Yes—
monitor.upA down or degraded monitor recovered.monitor, check, downtimeSeconds, incident {id,url,resolved} | null, ai (recovery report or null)Yes—
monitor.degradedA monitor became degraded (slow, a soft assertion failed, TLS expiring).monitor, checkYes—
monitor.reminderA monitor is still down at one of its reminderMinutes.monitor, downForMinutes, reminderAfterMinutes, incident {id,title}Yes—
incident.createdAn incident was opened, automatically or by a person or agent.Channels: Manual incidents. Automatic ones go to subscribers only (your team gets monitor.down).incident, updateYesYes
incident.updatedA public update was posted, or an automatic incident grew or partially recovered.Channels: Manual updatesincident, updateYesYes
incident.resolvedAn incident was resolved.Channels: Manual resolves. Automatic ones go to subscribers only (your team gets monitor.up).incident, update, durationMinutes, durationTextYesYes
maintenance.scheduledA maintenance window was scheduled.incident (kind: maintenance), updateYesYes
maintenance.startedA window reached scheduledStart.incident, updateYesYes
maintenance.updatedA public update was posted during maintenance.incident, updateYesYes
maintenance.completedA window reached scheduledEnd.incident, updateYesYes
incident.acknowledgedSomeone acknowledged an incident (API, MCP, dashboard or an alert link).Channels: The channels that were alerted about the incidentincident, ack {at, by, note?, via}Yes—
incident.escalatedAn escalation policy paged a level.Channels: Only that level's targets (channels, on-call people)incident, monitor?, policy {id,name}, level, levels, round, unacknowledgedForMinutes, notified {channelIds, users}——
incident.unacknowledgedAn acknowledgement was removed; reminders and escalation resume.Channels: Never (event stream only)incident, by——
component.status_changedA component's visible (effective) status changed, from any source or override.component, from, to—Webhooks only
monitor.createdA monitor was created.Channels: Never (event stream only)monitor——
monitor.deletedA monitor was deleted.Channels: Never (event stream only)monitor {_id, name}——
deploy.createdA deploy marker was recorded.Channels: Channels that list it (or "*")deploy——
digest.weeklyThe weekly digest was built (Mondays 08:00 workspace time).Channels: Channels that list it ("digest.*" or "*")the full digest (summary, monitors, incidents, trends, focus …)——
test.pingYou tested a channel with POST /channels/:id/test.Channels: Only the tested channelmessage, channel {id,name}——
  • Default channel: delivered to alert channels that have no explicit events list. Others need to be listed explicitly or matched by a wildcard ("*", "component.*").
  • Subscribers: delivered to status page subscribers, for public incidents only, and filtered by their components.
  • Monitor events only reach the channels attached to that monitor.

Payload envelopes for each delivery target are on the Webhooks page. In the event stream, data is the same object team webhooks receive.

Polling

GET /events (MCP: events_list) reads the event stream with an API key. It suits agents without a public webhook endpoint, and catching up after downtime.

curl "https://upbutler.com/api/v1/events?types=monitor.down,monitor.up&limit=2" \
  -H "Authorization: Bearer $UPBUTLER_API_KEY"
200 OK
{
  "data": [
    {
      "id": "evt_0n3qb7c21k4m6p8r0t2",
      "type": "monitor.up",
      "at": "2026-10-09T08:26:02.311Z",
      "data": { "monitor": { "_id": "mon_0n3q8kz1m4hx7c2v9rt", "name": "Search API", ... }, "downtimeSeconds": 811, ... }
    },
    {
      "id": "evt_0n3q9a12c4r7m1t8wxe",
      "type": "monitor.down",
      "at": "2026-10-09T08:12:31.880Z",
      "data": { "monitor": { ... }, "check": { ... }, "incident": { ... }, "ai": { ... } }
    }
  ],
  "next": "evt_0n3q9a12c4r7m1t8wxe"
}
QueryMeaning
afterOnly events after this id, oldest first. Without it, the newest events come first
typesComma-separated filter, such as monitor.down,monitor.up (exact types, no wildcards)
limit1–500, default 100

next is the id of the last event in data, or your after value when nothing new arrived. With after, feed next back in as the cursor. Ids are time-sortable, so “after” means “newer than”.

const API = "https://upbutler.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.UPBUTLER_API_KEY}` };

// Start at "now": the newest event id (the list is newest-first without "after").
const first = await (await fetch(`${API}/events?limit=1`, { headers })).json();
let cursor: string | null = first.data[0]?.id ?? null;

while (true) {
  const url = new URL(`${API}/events`);
  if (cursor) url.searchParams.set("after", cursor); // oldest-first after the cursor
  url.searchParams.set("limit", "500");
  const page = await (await fetch(url, { headers })).json();

  for (const event of page.data) await handle(event); // in order
  cursor = page.next ?? cursor;                      // persist this somewhere durable

  if (page.data.length < 500) await Bun.sleep(15_000); // caught up: wait a bit
}

async function handle(e: { id: string; type: string; data: any }) {
  if (e.type === "monitor.down") console.log("DOWN", e.data.monitor.name, e.data.ai?.analysis ?? "");
}

Live stream (SSE)

GET /api/v1/stream pushes the same events as they happen, as Server-Sent Events. It's the push alternative to polling /events or hosting a webhook endpoint. Auth is the same as the REST API: Authorization: Bearer <key> or the dashboard session cookie.

curl -N "https://upbutler.com/api/v1/stream?types=monitor.*,incident.created" \
  -H "Authorization: Bearer $UPBUTLER_KEY"
Stream
id: evt_0n3q9a12c4r7m1t8wxe
data: {"id":"evt_0n3q9a12c4r7m1t8wxe","type":"monitor.down","at":"2026-10-09T09:12:19.000Z","data":{…}}

: ping
OptionMeaning
typesComma-separated filter. Prefix wildcards are allowed: monitor.*,incident.created
after / Last-Event-IDResume after this event id. EventSource sends the header automatically on reconnect
checks=1Also send live-only check.completed frames for every finished check. They have no id and can't be replayed
  • Each frame is a default message event with id: <event id> and data: {"id","type","at","data"}, the same shape as GET /events.
  • A : ping comment is sent every 15 seconds to keep proxies from closing the connection.
  • The server closes the stream after about 10 minutes. Reconnect with the last id you saw and nothing is lost.
stream.ts
// Reads the SSE stream with fetch so you can send the Authorization header.
let lastId: string | null = null;
while (true) {
  const res = await fetch("https://upbutler.com/api/v1/stream?types=monitor.*", {
    headers: { Authorization: `Bearer ${process.env.UPBUTLER_API_KEY}`, ...(lastId ? { "Last-Event-ID": lastId } : {}) },
  });
  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
  let buf = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break; // server closes after ~10 min: reconnect and resume
    buf += value;
    let i;
    while ((i = buf.indexOf("\n\n")) >= 0) {
      const frame = buf.slice(0, i); buf = buf.slice(i + 2);
      const id = frame.match(/^id: (.*)$/m)?.[1];
      const data = frame.match(/^data: (.*)$/m)?.[1];
      if (id) lastId = id;
      if (data) handle(JSON.parse(data)); // {id, type, at, data}
    }
  }
}