Webhooks

Webhooks

Deliver project lifecycle events to HTTPS endpoints.


Webhooks deliver project lifecycle events to systems that need durable notifications outside a live signaling connection. They are available on Pro and Enterprise projects and are configured per tunnel project from the dashboard or the Control plane API.

Tunnel clients and published tunnels are runtime resources. They can appear, disappear, and move between processes quickly. Webhooks attach durable application behavior to those changes: updating an inventory, persisting metadata from labels, triggering provisioning, or cleaning up state when a tunnel is removed.

Webhooks are separate from project logs. Lifecycle events describe resources such as clients and tunnels. Logs describe traffic and connection summaries. Webhook deliveries describe attempts to send one lifecycle event to one configured destination.

Event catalog

The launch catalog contains these lifecycle events:

EventDescription
client.createdA tunnel client connected and authenticated against the project.
client.deletedA tunnel client disconnected or was removed from the active project state.
tunnel.createdA tunnel was created and became available for the project.
tunnel.deletedA tunnel was deleted or stopped publishing for the project.

Traffic logs, connection logs, and stream.summary records are not emitted through webhooks.

Payload

Each request body is the canonical event JSON:

{
  "id": "event_id",
  "type": "tunnel.created",
  "created_at": "2026-06-01T12:00:00Z",
  "workspace_id": "workspace_id",
  "project_id": "project_id",
  "cluster_id": "cluster_id",
  "user_id": "user_id",
  "object": {}
}

The id field is the event idempotency key. The delivery id header distinguishes repeated delivery attempts for the same event.

Delivery behavior

rstream stores lifecycle events and delivery records before attempting delivery. The engine dispatcher leases due deliveries, sends HTTPS requests, records each attempt, and retries failed deliveries with exponential backoff.

Delivery is at least once. Receivers use the event id as an idempotency key because recovery and retries can produce more than one delivery attempt for the same event.

Webhook endpoints must return a 2xx status code for success. Other status codes, network errors, and request timeouts are recorded as failed attempts. Response bodies are retained only up to the configured cap.

Delivery history follows the project event retention window. Delivery attempts are attached to their delivery record and are removed when the delivery expires.

Signatures

Each webhook request includes:

HeaderPurpose
rstream-signatureHMAC signature for the raw request body.
rstream-event-idCanonical lifecycle event id.
rstream-event-typeLifecycle event type.
rstream-webhook-idWebhook endpoint that received the delivery.
rstream-delivery-idDelivery record id for this webhook endpoint.

The signature header uses this shape:

t=<unix_timestamp>,v1=<hex_hmac>

The HMAC is SHA-256 over:

<unix_timestamp>.<raw_request_body>

Verify against the raw body bytes before parsing JSON. The JavaScript SDK performs signature verification and schema parsing in the same call, so the receiving route can keep the trust boundary small:

import { Buffer } from "node:buffer";
import { RstreamWebhookResource } from "@rstreamlabs/tunnels";
import type { WebhookEvent } from "@rstreamlabs/tunnels";
 
const webhooks = new RstreamWebhookResource();
 
type ParsedWebhookEvent = WebhookEvent & { id: string; created_at: string };
 
function requireCanonicalFields(event: WebhookEvent): ParsedWebhookEvent {
  if (!event.id || !event.created_at) {
    throw new Error("Webhook event is missing canonical metadata.");
  }
  return event;
}
 
export async function parseRstreamWebhook(
  request: Request,
): Promise<ParsedWebhookEvent> {
  const secret = process.env.RSTREAM_WEBHOOK_SIGNING_SECRET;
  const signature = request.headers.get("rstream-signature");
  if (!secret || !signature) {
    throw new Error("Missing webhook secret or signature.");
  }
  const rawBody = Buffer.from(await request.arrayBuffer());
  const event = await webhooks.event(rawBody, signature, secret);
  return requireCanonicalFields(event);
}

After parsing, route the event by type. Labels are often the stable bridge between runtime resources and application records: for example a device id, customer id, environment, or service role.

import type { WebhookEvent } from "@rstreamlabs/tunnels";
 
type ParsedWebhookEvent = WebhookEvent & { id: string; created_at: string };
 
type LifecycleChange = {
  eventId: string;
  labels: Record<string, string>;
  observedAt: string;
  resourceId: string;
  state: "online" | "offline";
};
 
function lifecycleChange(event: ParsedWebhookEvent): LifecycleChange {
  const labels = event.object.labels ?? {};
  switch (event.type) {
    case "client.created":
    case "tunnel.created":
      return {
        eventId: event.id,
        labels,
        observedAt: event.created_at,
        resourceId: event.object.id,
        state: "online",
      };
    case "client.deleted":
    case "tunnel.deleted":
      return {
        eventId: event.id,
        labels,
        observedAt: event.created_at,
        resourceId: event.object.id,
        state: "offline",
      };
  }
}

During secret rotation, rstream may send more than one v1 signature in the same header. A request is valid when any signature matches an active secret.

Local receiver tests

rstream events --webhook is the local receiver development path for live project events. The CLI keeps the watch stream local, but sends the receiver the same JSON body and signed headers as a configured webhook delivery:

rstream events \
  --webhook \
  --webhook-secret "$RSTREAM_WEBHOOK_SIGNING_SECRET" \
  --events tunnel.created,tunnel.deleted \
  --forward-to http://localhost:3000/api/rstream/webhook

Passing the same secret to the CLI and the receiver keeps local verification repeatable. If no --webhook-secret is provided, the CLI prints a generated whsec_... secret to stderr for that receiver session. Add --include-webhook-headers when inspecting stdout instead of forwarding to an HTTP endpoint.

This mode is for local development. It does not create a webhook destination, persist delivery attempts, or retry after the CLI exits. Configured project webhooks provide durable delivery history and retry behavior.

Destinations

Configured webhook endpoints use HTTPS and cannot include embedded credentials. Managed delivery also rejects private, loopback, link-local, multicast, and metadata-service destinations.

For local receiver development, rstream events --webhook --forward-to can target an HTTP localhost endpoint without creating a persistent webhook destination.

API and MCP

Project events APIs inspect canonical lifecycle events. Webhook delivery APIs inspect per-webhook delivery state, attempts, response codes, response bodies, and response times.

MCP keeps the same boundary between the event stream and delivery diagnostics:

  • rstream_project_events_list lists canonical lifecycle events.
  • rstream_project_logs lists traffic and connection logs.
  • rstream_project_webhook_deliveries_list and rstream_project_webhook_delivery_get inspect webhook delivery context.