Skip to main content
Borough offers two mechanisms for going beyond a plain cached read:
  1. Listing stream (Pro+): a short-lived SSE stream that delivers cached data immediately, then one fresh live fetch
  2. Persistent watchers (Business+): Durable Object-backed subscriptions that poll on a schedule and emit change events

Listing Stream (SSE)

GET /v1/listing/{id} opens a Server-Sent Events stream for a single listing. The stream emits:
The cached event arrives immediately. The live event typically takes 2-5 seconds depending on upstream response time. If the live fetch fails, an error event is emitted with an error code and message, and the stream closes. This is a short-lived stream: it completes once cached + live data have been delivered, usually within about 10 seconds. Size your client timeout off the worst case rather than the typical one, because a struggling upstream can take considerably longer: a single live fetch is allowed 30 seconds, and an upstream rate-limit or unavailability buys one retry, so a stream can stay open for a little over a minute before it closes. Tier requirement: Pro, Business, or Internal.

Persistent Watchers

Watchers are long-lived subscriptions backed by Cloudflare Durable Objects. They support three watch types:

Creating a watcher

Poll intervals

Each poll costs one API request against your active quota window.

Streaming watcher changes

GET /v1/watchers/{id}/stream opens an SSE connection that emits change events: These five are the complete set. No other event type is emitted on this stream.
heartbeat is not tied to your watcher’s pollInterval. The stream drains the watcher’s change buffer on its own short, fixed cadence (currently about every 20 seconds) and emits a heartbeat whenever that drain turns up nothing new. Polling upstream happens separately, on the watcher’s own schedule. A heartbeat therefore means “the connection is alive and there is nothing new to hand you” — it does not mean a poll just completed.
The id on each event is a resume cursor — see Resuming without missing changes.

Managing watchers

  • List: GET /v1/watchers
  • Detail: GET /v1/watchers/{id}
  • Pause/resume: PATCH /v1/watchers/{id} with {"active": false} or {"active": true}
  • Change interval: PATCH /v1/watchers/{id} with {"pollInterval": 600}
  • Delete: DELETE /v1/watchers/{id}

Stream duration limit

Watcher SSE streams have a 5-minute maximum duration. When the limit is reached, the server sends a reconnect event and closes the stream:
Clients should automatically reconnect when receiving this event.

Resuming without missing changes

The watcher keeps a bounded buffer of the changes it has detected — the last 50 — and every change, heartbeat, and reconnect event carries an SSE id that is a cursor into that buffer. Replay the cursor on your next connection and the server resumes from there instead of from the current tip, so changes detected while nothing was connected are delivered rather than skipped. There are two ways to send the cursor: If both are present, Last-Event-ID wins — on an automatic reconnect it is the fresher of the two. A cursor must be a non-negative integer; anything else is treated as absent, which is the same as a fresh connect.
A few things to keep in mind:
  • A connection with no cursor starts at the buffer tip. You get changes detected from that point on, not the watcher’s history. That is deliberate — otherwise every reconnect would replay the retained buffer as fresh alerts.
  • Retention is bounded. If more than 50 changes accumulated while you were away, only what survives in the buffer can be replayed. A client that stays away that long gets what is left, not everything it missed.
  • change ids are per-change, so a client that drops mid-batch resumes at the change it actually received rather than at the start of the batch.
  • The connected event carries no id — it is stream metadata, not a buffer position.

Quota impact

Each watcher alarm poll costs 1 API request toward your active quota window and counts toward metered overage billing. A watcher polling every 15 minutes consumes approximately 2,880 requests per 30 days. Listing watchers also make a live upstream fetch on each poll. Building and search watchers diff Borough’s cached index instead, so they avoid a live fetch on every cycle. If your quota (including overage headroom) is exhausted, the watcher is automatically paused: its active field flips to false and it stops polling. An open SSE stream sees this on its next drain and emits a stopped event, then closes. There is no dedicated quota event on the SSE stream, and the stopped payload does not say why the watcher stopped — a quota pause, a PATCH with {"active": false}, and a delete all produce the same event. To distinguish them, read GET /v1/watchers/{id} (a deleted watcher returns 404) and check your usage. Reconnecting to a paused watcher returns 400 until you reactivate it. Tier requirement: Business or Internal.

Choosing the Right Approach

Business and Internal tiers get live-first data automatically on GET /v1/property/{id} and GET /v1/building/{id}: no SSE stream needed for single lookups. The regular endpoint returns "source": "live" when data is fetched fresh. See Data Freshness for details.