Skip to main content
Register a URL and Daydream will POST to it when a stream starts and when it stops. This is the reliable way to learn that a session ended — including the cases where nothing tells you otherwise, like a viewer backgrounding the tab, closing their laptop, or dropping off Wi-Fi.
Polling GET /v1/streams/{id}/status only works while a stream is live. Once the pipeline is torn down the gateway returns state: OFFLINE with no timestamp, so there is nothing left to read after the fact. The stream.ended webhook carries that timestamp to you instead.

Quickstart

1

Register your endpoint

The response contains your signing secret in full, once:
Store secret now. Later reads return only a secret_preview; if you lose it, your only option is POST /v1/webhooks/{id}/rotate-secret.
2

Verify it reaches you

This sends a synthetic, fully-signed stream.ended and returns exactly what your endpoint responded with — status, body, and round-trip time. Use it to check both connectivity and your signature verification before real traffic arrives.
3

Handle the event

Respond 2xx quickly. Anything else — or no response within 10 seconds — counts as a failure and gets retried.

Events

A stream that reconnects emits a fresh stream.started, so a single stream id can cycle startedendedstarted over its lifetime.

Envelope

Every event has the same shape:

stream.started

stream.ended

ended_at is the last moment the stream was confirmed live, not the moment we noticed it had stopped. Those are different: detection takes up to 45 seconds after the last signal, and ended_at is backdated past that window. It is the timestamp to bill and report against. reason is one of:

Verifying signatures

Every request carries these headers: The signature is an HMAC-SHA256 over `${timestamp}.${rawBody}` keyed with your secret. Sign the raw request body, before any JSON parsing — re-serializing changes the bytes and the signature will not match.
Compare signatures with a constant-time function (timingSafeEqual, hmac.compare_digest), never ==. Reject events whose timestamp is more than a few minutes old, which is what stops a captured request from being replayed.

Delivery, retries, and idempotency

A delivery is one POST to one endpoint. Non-2xx, a timeout past 10 seconds, or a connection error all count as failures and are retried with exponential backoff:
Eight attempts spanning roughly an hour. Redirects are not followed — a 3xx is a failure. Deduplicate on id. Retries reuse the same event id, and a delivery that timed out on your side may well have been processed. Treat handlers as idempotent. If an endpoint fails 20 deliveries in a row it is automatically disabled and stops receiving events. Any successful delivery resets that counter. Re-enable with:
which also clears the failure count.

Debugging deliveries

Each delivery is recorded for 14 days: the exact payload sent, how many attempts it took, and the status and body your endpoint returned. The record is updated in place as retries happen, so attempts is a running count while response_status, response_body and error describe the most recent attempt rather than each one individually.
Filter by status (pending, succeeded, failed), by event_type, or by stream_id to answer “did the stream.ended for this specific stream reach me?”:
pending means retries are still in flight; failed means every attempt was used up.

Endpoint requirements

  • HTTPS only. Plaintext http:// is rejected.
  • Publicly routable. Loopback, private (RFC1918), link-local and carrier-grade NAT addresses are rejected at registration. For local development, put a tunnel (ngrok, Cloudflare Tunnel) in front of your server.
  • Respond within 10 seconds. Acknowledge with a 2xx first and do the real work asynchronously; a slow handler turns into a retry storm.
  • Up to 10 endpoints per account. Useful for pointing production, staging and a test receiver at the same events.

Managing endpoints

Rotating a secret takes effect immediately — the old one stops signing anything the moment the call returns. If you verify signatures strictly, deploy code that accepts the new secret before you rotate.