Skip to content
Beam Docs

Room Webhook Members

Let any HTTPS endpoint take part in a Beam Room through a member's agent: set it up, verify its signed requests, reply, and know what its host can read.

A webhook member lets any system that receives HTTPS webhooks take part in a Room. It is a visible member of the Room with its own channel grants, but it runs no Beam software. Another member's always-on agent, its host, runs it: the host decrypts what the webhook is sent, POSTs it to the webhook's URL, and publishes the receiver's replies in the Room as the webhook.

The receiver can be anything that accepts an HTTPS POST: an internal service, an automation platform, an alerting system, or an AI service. This page describes the HTTP contract the receiver implements; it does not depend on any particular product.

Trust model

The host can read everything sent to the webhook and can publish in the Room as the webhook. Room content stays end-to-end encrypted up to the host's agent; from there it travels to your receiver as plaintext inside HTTPS. Beam infrastructure never decrypts webhook traffic. The URL and the signing secret exist only on the host: Beam's coordinator sees the URL's host name and a fingerprint, never the URL itself or the secret. Choose a host you trust with everything the webhook can receive.

Webhook members need Beam CLI 0.1.37 or later. Durable delivery and asynchronous replies need 0.1.40 or later on the host's machine, and members need 0.1.40 to see the durable setting in webhook show. Keep the CLI and its agent current with beam update.


What a webhook member can do

A webhook canA webhook cannot
Receive live messages on message channels it subscribes to.Hold manage, roles, or grants on any channel kind other than message and object.
Receive files on object channels it subscribes to, with their bytes up to a size cap.Publish files: it is never the source of an object publication.
Reply to a message it received, on the same channel, when it holds publish there: in its response, or later.Publish on its own initiative: every message it publishes is a reply.
Have its host keep undelivered messages and files, encrypted, and retry them for up to 24 hours.Receive anything published while its host is offline.
Appear in beam room <room-id> member list with kind webhook.

Its grants are limited accordingly: discover, subscribe, and publish on message channels, and discover and subscribe on object channels.


Before you start

  • A host. The host is an active agent member of the Room whose agent is connected to a Beam account (beam agent connect). A machine that joined only through an invitation cannot host, and neither can a Studio-managed agent. Delivery runs only while the host's agent is online, so pick a machine that stays on.
  • Current agents on the webhook's channels. A webhook receives a channel's encryption keys only from a member that holds manage on that channel. Every such member must run an agent with webhook support: see Members that must upgrade.
  • Live message channels. A webhook receives live messages, so a message channel granted to it must allow retention none. Channels allow it unless they were created with a persistence policy that leaves it out. For durable delivery, the channel must also allow receiver_local.
  • Room owner or admin. Adding, moving, and removing a webhook needs the Room owner or admin role. Any member can list and show webhooks.
  • Limits. A Room holds at most 10 webhooks, and one host agent hosts at most 20 across all its Rooms. Pending webhooks count toward both.

Set up a webhook member

1. Create the shared secret

The receiver and the host share a signing secret of 16 to 512 bytes. Generate it on the host's machine, give the same value to your receiver, and keep it out of shell history and logs:

openssl rand -hex 32 > webhook.secret
chmod 600 webhook.secret

The host uses the secret exactly as written, without its trailing line ending. It is not decoded: the HMAC key is the 64 hex characters themselves.

2. Add the webhook

A Room owner or admin adds the webhook, names its host, and grants its channels:

beam room <room-id> webhook add \
  --name assistant \
  --host <host-member-id> \
  --channel <channel-id>=discover,subscribe,publish

--channel is repeatable, one channel and its actions each time. Grant publish on a channel only when the receiver should reply there. Other options:

OptionEffect
--payload-mode rawPOST the published body unchanged instead of a JSON envelope. See Payload modes.
--max-file-mb <1-25>The largest file sent with its bytes, in MiB (default 25). A larger file is announced by its metadata only.
--idempotency-key <key>Makes a retried add safe.
--quietPrint only the new webhook's member ID.

The webhook is pending until its host accepts it.

3. Accept it on the host

On the host's machine, its owner lists what the machine is asked to host and accepts the webhook with its URL and secret:

beam webhook hosted
beam room <room-id> webhook accept <webhook-id> \
  --url https://receiver.example.com/beam \
  --secret-file webhook.secret

accept shows the trust model and asks for confirmation; pass --yes only when no one can answer and the trust model has been approved. The secret comes from a private file or from standard input with --secret-stdin, typed without echo on a terminal. It is never accepted as a command-line value and never printed.

  • The URL must be an absolute https URL, without user information or a fragment.
  • --allow-private lets it reach a private address, such as a service on the host's own network. See Network rules.
  • --durable turns on durable delivery.
  • --reply-window <duration> sets how long the host waits for an asynchronous reply, 1m to 24h (default 1h).
  • accept also takes --payload-mode and --max-file-mb.
  • beam room <room-id> webhook decline <webhook-id> refuses the webhook instead, which removes it.

4. Test and confirm

Send a signed test event to the receiver:

beam room <room-id> webhook test <webhook-id>

test reports the outcome and exits non-zero unless the receiver accepted the request. It does not change the webhook's state. Then check that the webhook delivers:

beam room <room-id> webhook show <webhook-id>

show says Delivering. once the webhook is online and holds the keys of all its channels, which takes a few seconds after an accept. See States and presence.


Receiving requests

Every delivery is an HTTPS POST with the user agent Beam-Webhook/1. The X-Beam-Event header names the event:

EventSent when
messageA message is published on a message channel the webhook subscribes to.
fileA file on an object channel the webhook subscribes to is within the size cap; the request carries its bytes.
file.noticeThe file is larger than the size cap; the request carries its metadata only.
testThe host's owner runs webhook test.

A webhook receives what is published while its host's agent is online; nothing is kept for it while the host is offline. Without durable delivery, the host tries each delivery a few times within seconds and keeps nothing across a restart of its agent. With it, the host keeps what it accepted, encrypted on its own disk, and retries it for up to 24 hours.

Payload modes

ModeMessageFile within the capLarger fileTest
envelope (default)JSON envelopemultipart/form-data: an envelope part (JSON) and a file partJSON envelope of type file.noticeJSON envelope of type test
rawThe published body unchanged, with its content typeThe file's bytes, application/octet-streamEmpty bodyEmpty body

In raw mode the metadata travels only in the headers.

A message envelope looks like this:

{
  "version": 1,
  "type": "message",
  "id": "btr_pub_...",
  "room_id": "btr_room_...",
  "channel_id": "btr_chan_...",
  "webhook_member_id": "btr_member_...",
  "publication_id": "btr_pub_...",
  "publisher_member_id": "btr_member_...",
  "content_type": "application/json",
  "hop": 0,
  "occurred_at": "2026-10-09T12:00:00Z",
  "text": "{\"hello\":\"world\"}"
}
  • The message body is in text when it is valid UTF-8, and in payload_base64 otherwise.
  • reply_to_publication_id is present when the message is itself a reply.
  • A file or file.notice envelope carries file: {name, size_bytes, attached, max_file_bytes}, where attached says whether the request carries the bytes. publisher_member_id is the member that published the file.
  • A test envelope has a random id and no channel or publication.

Headers

HeaderValue
X-Beam-Eventmessage, file, file.notice, or test.
X-Beam-RoomThe Room ID.
X-Beam-Webhook-MemberThe webhook's member ID.
Idempotency-KeyThe publication ID, or the test ID. It is the same on every attempt of one delivery.
X-Beam-ChannelThe channel ID, when there is one.
X-Beam-PublicationThe publication ID, when there is one.
X-Beam-PublisherThe member that published it, when known.
X-Beam-HopThe message's hop count. Messages only.
X-Beam-Reply-ToThe publication this message replies to, when it is a reply.
X-Beam-File-NameThe file name, percent-encoded. Files only.
X-Beam-File-SizeThe file size in bytes. Files only.
X-Beam-TimestampThe Unix time, in seconds, that the signature covers.
X-Beam-SignatureOne or two v1= signatures. See Verify the signature.

Verify the signature

Every attempt is signed with its own timestamp:

X-Beam-Timestamp: 1760000000
X-Beam-Signature: v1=<hex HMAC-SHA256(secret, "v1:<timestamp>:<body>")>

<body> is the raw request body exactly as received, before any JSON or multipart parsing. While the host rotates its secret, the header carries the new secret's signature, a comma, then the previous secret's: v1=<new>,v1=<old>.

Accept a request when both hold:

  1. The timestamp is within 300 seconds of your clock.
  2. At least one v1 signature matches one of your secrets. Compare in constant time and ignore any scheme other than v1.

A Python receiver can check both with the standard library:

import hashlib
import hmac
import time

WINDOW_SECONDS = 300


def verify_beam_signature(
    secrets: list[bytes], timestamp: str, signature: str, body: bytes
) -> bool:
    """Check X-Beam-Timestamp and X-Beam-Signature against the raw body."""
    try:
        signed_at = int(timestamp.strip())
    except (AttributeError, ValueError):
        return False
    if abs(time.time() - signed_at) > WINDOW_SECONDS:
        return False
    message = b"v1:" + str(signed_at).encode() + b":" + body
    expected = [hmac.new(s, message, hashlib.sha256).hexdigest() for s in secrets]
    for candidate in signature.split(","):
        scheme, _, value = candidate.strip().partition("=")
        if scheme != "v1":
            continue
        if any(hmac.compare_digest(value.lower(), digest) for digest in expected):
            return True
    return False

Pass the secret as bytes, exactly as the host has it (for the file above, open("webhook.secret", "rb").read().rstrip(b"\r\n")). Answer 401 when the check fails: the host then marks the webhook degraded with reason auth, which tells the Room that the two sides disagree on the secret.


Respond

Each attempt has 10 seconds, from connecting to the end of the response. Answer within that time.

ResponseLive deliveryDurable delivery
2xxDelivered.Delivered, and deleted from the host.
5xx, 408, 429, a network error, or a timeoutRetried: up to 3 attempts in all, 1 second and then 2 seconds apart.Retried for up to 24 hours, at most 5 minutes apart, or as the Retry-After of a 429 or 503 asks.
401 or 403Failed, not retried. The webhook becomes degraded with reason auth.Retried like a 5xx, and the webhook becomes degraded with reason auth.
410Failed, not retried. The webhook is paused at once with reason gone. Use it to stop deliveries.The webhook is paused with reason gone. The message waits for a resume within its 24 hours; after that it is a dead letter to replay.
Any other status, including a redirectFailed, not retried.Kept as a dead letter.

Other deliveries go on while one is retried.

Retries carry the same Idempotency-Key, so drop a delivery whose key you have already processed. When the work takes longer than a few seconds, record the request, answer 2xx at once, and do the work afterwards, or reply asynchronously.

A publisher sees the webhook as reached once the host has accepted the message for delivery, not once your receiver answered. Whether your receiver accepted it shows in the webhook's state and, on the host, in beam webhook hosted --json, which adds the local delivery state and the recent deliveries.

Reply

Delivery and reply. The service joins the Room as a member without running Beam. Room traffic stays encrypted up to its host; the host signs and POSTs it, and publishes the reply as the webhook.

A receiver can answer a message in the Room by opting in on its response. The response is a reply when it has all of:

  • the header X-Beam-Reply: v1;
  • Content-Type: application/json;
  • a JSON body of at most 64 KiB;
  • a 2xx status.
HTTP/1.1 200 OK
Content-Type: application/json
X-Beam-Reply: v1

{"answer": "The nightly export finished at 02:14 UTC."}

The host publishes the body as the webhook, live, on the channel the triggering message arrived on, with its hop one more than the trigger's and reply_to_publication_id set to the trigger. The webhook therefore needs publish on that channel. Only members listening at that moment receive the reply, and the webhook never receives its own replies.

  • A response without the header is a plain acknowledgement. A receiver that cannot set response headers cannot reply.
  • A file delivery never produces a reply.
  • Replies are limited to 30 per minute per webhook, with bursts of 10.
  • A reply goes out only while the webhook may still receive on the trigger's channel.

On the host, each delivery record in beam webhook hosted --json shows what became of its reply: queued, published, failed, rate_limited, queue_full, ignored (with the reason the response was not a valid reply), object_channel (the trigger was a file), revoked, or one of the asynchronous outcomes below.

Asynchronous replies

Asynchronous reply. A service that needs minutes answers 202 Accepted with X-Beam-Reply: v1 and a Location. The host polls it with signed requests and publishes the reply in the Room once it is ready.

A receiver that needs longer than the 10-second attempt answers 202 Accepted with X-Beam-Reply: v1 and a Location header, and serves the reply there when it is ready. The host polls that URL until it gets the reply or the webhook's reply window ends, and publishes the reply like an immediate one.

HTTP/1.1 202 Accepted
X-Beam-Reply: v1
Location: /jobs/7f3a
Retry-After: 30
  • Location may be relative. It must be an https URL on the webhook URL's origin: the same host and port. Otherwise nothing is polled, and the record shows ignored with async_location_cross_origin or async_location_invalid.
  • A 202 without X-Beam-Reply: v1 is a plain acknowledgement, and nothing is polled.
  • Each poll is a GET signed like a delivery whose body is empty: verify v1:<timestamp>: with the same secret. It carries X-Beam-Webhook-Member, X-Beam-Room, X-Beam-Channel, and X-Beam-Publication (the message being answered), follows the network rules, and does not follow redirects.
  • The host polls at your Retry-After (on a 202, 429, or 503). Without one, it polls every 30 seconds while the reply is pending, and after a failed poll it backs off from 5 seconds up to 5 minutes. It never polls more often than every 5 seconds.
  • The reply window is 1 hour unless the host's owner sets another, from 1 minute to 24 hours, with beam room <room-id> webhook reply-window <webhook-id> <duration>. A new window applies to later 202 responses; replies already polled for keep theirs.
Your answer to a pollThe host
202Polls again.
2xx with X-Beam-Reply: v1, application/json and a JSON body of at most 64 KiBPublishes the reply, and stops polling.
204, or another 2xxStops polling, without a reply.
404, 410, a redirect, or another 4xxStops polling.
5xx, 401, 403, 408, 429, a network error, or a timeoutPolls again.

The host keeps polling through restarts of its agent. A paused webhook does not poll; its replies wait for a resume, within their window. A webhook that loses access to the channel stops polling at once and publishes nothing more. The host publishes each reply it fetches at most once: if its agent stops after fetching a reply and before publishing it, that reply is lost rather than published twice. A message delivered again, for example because your answer to it was lost, can start a second reply, so deduplicate on Idempotency-Key before starting work.

The delivery record shows polling, then queued and published, or none (finished without a reply), abandoned, expired (still pending when the window ended), revoked, or failed. webhook test says when a response would start an asynchronous reply; a test polls nothing.

Loops and rate limits

Refusal seen by the sender. When the host refuses a message for the webhook, the sender is told which webhook and why, instead of a silent "delivered". Other members still receive it.

Messages that members publish have hop 0 unless the publisher sets a hop, and a reply has its trigger's hop plus one. A host does not deliver a message at hop 2 or more to a webhook, so a reply can reach at most one more webhook, and two webhooks cannot answer each other indefinitely. Members still receive those messages.

Each webhook receives at most 60 deliveries per minute, with bursts of 20. Without durable delivery, its host holds at most 64 deliveries, waiting or in progress. A message over the hop limit is skipped. The host refuses a message when the webhook is paused, when it exceeds the rate limit, or when the queue is full. A refusal does not count as a webhook failure, and its publisher sees it: the publish result lists the webhook member in member_failures with webhook_paused, webhook_rate_limited or webhook_queue_full. The host does not keep a refused message.

A service that publishes what a webhook received into another Room starts a new message, which escapes the hop limit unless the service carries the hop forward; see Bridging Rooms.


Delivery guarantees

Room webhooks provide at-least-once delivery for events accepted into a customer-controlled host's encrypted local spool, within configured storage and retention limits. Beam infrastructure does not retain customer content.

The guarantee covers a webhook with durable delivery on, for the messages and files of channels that allow receiver_local. It does not cover:

  • events published while the host is offline;
  • events rejected before spool acceptance, which appear in the publisher's member_failures;
  • a refused file, which shows only in the host's delivery records;
  • kept events the host deletes because the webhook lost access to the channel, or the channel stopped allowing receiver_local.

Without durable delivery, delivery is best effort: a few attempts within seconds, and nothing kept across a restart of the host's agent.

Ordering. Webhook events may be delivered out of order. Receivers must not rely on delivery order; applications that need ordering implement their own sequencing.

Deduplication. Receivers deduplicate on Idempotency-Key. At-least-once means a delivery can arrive more than once, for example when the receiver processed it but its answer was lost, and every attempt of one delivery carries the same key.


Durable delivery

Durable delivery. With durable delivery on, the host keeps each message of a channel that allows it on its own disk, encrypted, before acknowledging it, and retries it for up to 24 hours. Beam never holds a copy.

With durable delivery on, the host writes each message and file it accepts for the webhook to an encrypted spool on its own disk before acknowledging it, and retries it for up to 24 hours. The spool stays on the host's machine; Beam never holds a copy. The host's owner turns it on:

beam room <room-id> webhook durable <webhook-id> on

or at accept with --durable. off keeps nothing new; what is already kept is still delivered. Every member sees the setting in beam room <room-id> webhook show <webhook-id>: Durable: on, its host keeps undelivered events… or Durable: off, live delivery only.

  • Which channels. Only channels whose persistence policy allows receiver_local are kept. Such a channel needs a maximum size and age, which bound what the host keeps for it. The maximum age counts from arrival and caps both the retries and the dead letters: the full 24 hours of retries and 7 days of dead letters need a maximum age of at least 192 hours.

    beam room channel create <room-id> --name events --kind message \
      --persistence-modes none,receiver_local \
      --persistence-max-bytes 104857600 --persistence-max-age 192h

    On other channels the webhook keeps delivering live. The command that turns durable delivery on says how many channels qualify; beam webhook hosted --json lists them in durable_channel_ids.

  • Retries. A message is retried for up to 24 hours, at most 5 minutes apart, or as the Retry-After of a 429 or 503 asks, through restarts of the host's agent, always with the same Idempotency-Key. Then it becomes a dead letter, kept for 7 days. The channel's maximum age shortens both. The 24 hours keep running while the webhook is paused.

  • Dead letters. A message the receiver keeps failing for 24 hours, or rejects for good (a redirect, or a 4xx other than 401, 403, 408, 410, or 429), becomes a dead letter:

    beam room <room-id> webhook dead-letters list <webhook-id>
    beam room <room-id> webhook dead-letters replay <webhook-id> <publication-id>
    beam room <room-id> webhook dead-letters purge <webhook-id> --all
  • Space. One host keeps 1 GiB in all by default; set BEAM_TUNNEL_WEBHOOK_SPOOL_MAX_MIB in the environment of the host's agent to change it. The space is divided equally among the webhooks that deliver durably at that moment, and a channel's maximum size bounds what it keeps. Dead letters count toward the space. A message past either limit is refused, and its publisher sees webhook_spool_full in member_failures; webhook_spool_unavailable means the host could not write it.

  • Files. A file up to the webhook's file cap (25 MiB at most) is kept with its bytes. A larger file is delivered as a file.notice, with its metadata only.

  • Access. Before every attempt the host checks that the webhook may still receive the channel. When it can't confirm that, for example while it cannot reach Beam, it holds the messages and resumes by itself. When the webhook loses access, see When access changes.

  • Order. Kept messages are delivered in no particular order; see Delivery guarantees.

On the host, beam webhook hosted shows the messages kept, the dead letters, and the space they use.


Run the host next to your receiver

A webhook delivers while its host's agent runs, and durable delivery keeps messages on that host's disk. To keep both under your control, run the host on a server in your own infrastructure, next to the receiver:

  1. Install the CLI on that server and connect its agent to your Beam account: curl -fsSL https://cdn.b1m.ai/cli/install.sh | sh, then beam agent connect. An agent that joined only through an invitation cannot host.

  2. Join the Room as that machine with beam room join <room-id> --invitation-file invitation.token, and keep its agent running: start it with beam agent start, and from your service manager when the server boots.

  3. Have a Room owner add the webhook with this machine's member as its host.

  4. Accept it there with durable delivery:

    beam room <room-id> webhook accept <webhook-id> \
      --url https://receiver.internal.example.com/beam \
      --secret-file webhook.secret --durable --allow-private

    --allow-private is needed only when the receiver's address is private. The receiver still needs HTTPS with a certificate the server trusts.

  5. Give the webhook's channels receiver_local with a size and age limit, as in Durable delivery, and check with beam room <room-id> webhook show <webhook-id>.


Network rules

The host sends each request directly to the receiver:

  • HTTPS only, with TLS 1.2 or later and a certificate the host machine trusts.
  • No proxy, and redirects are not followed: a 3xx response is a failed delivery.
  • Each connection resolves the URL's host name, refuses it when any answer is a blocked address, and connects to the first answer, so a later DNS answer cannot move the connection.
  • Without --allow-private, private, loopback, link-local, carrier-grade NAT, unique local, and other special-purpose addresses are blocked, including when written as IPv4-mapped, NAT64, or 6to4 addresses.
  • Always blocked, even with --allow-private: unspecified, broadcast, multicast, and reserved addresses, Teredo, and cloud instance metadata services.
  • The host's logs name the webhook member and the URL's host name only, never the URL, its query, the secret, or a payload.

States and presence

beam room <room-id> webhook list shows each webhook's state and reason, and whether it delivers now (DELIVERY). show explains the state and what to do.

StateMeaningWhat to do
pendingWaiting for its host to accept it with the URL and secret.Accept it on the host's machine.
activeDelivering.
degradedStill delivering, after 5 failed delivery attempts in a row (failing) or a rejected signature (auth, HTTP 401 or 403).Fix the receiver, or rotate the secret. A successful delivery returns it to active.
pausedNot delivering: an HTTP 410 response (gone), or its host left or was removed (host_left, host_removed).resume it, or move it when it has no host.

Failures are counted per attempt, and they never pause a webhook, however long they last. After 5 failed attempts in a row, the host sends one attempt at a time until one succeeds. Time the host spends offline never counts toward a failure state.

An active or degraded webhook delivers only while both it and its host are online, and only on the channels whose current keys it holds. Its state does not change meanwhile:

list showsMeaning
host offline: not deliveringThe host's agent is offline. Delivery resumes when it is back online.
webhook offline: not deliveringThe host is online but does not run the webhook yet. The host's agent brings it online seconds after it starts or accepts the webhook; beam webhook hosted on the host shows whether it runs it.
joining encryption: not delivering on N channelsThe webhook does not hold the current encryption keys of those channels yet, after an accept, a host move, or another membership change. It catches up within seconds; show names the channels.

With --json, a webhook carries presence (its own) and host_presence (its host's), each online or offline, and keys_pending_channel_ids. The webhook delivers on all its channels when both are online and the list is empty.

Stopping the host's agent with beam agent stop takes the host and its webhooks offline in their Rooms at once, and they come back online within seconds of the next start. After a crash, they go offline when their leases expire.

When access changes

Access revoked or held. The host re-checks the webhook's access before every attempt. Revoking stops delivery and deletes what it kept; when access can't be confirmed, delivery waits.

Before every delivery attempt, retries included, the host checks that the webhook may still receive the message's channel. It uses the Room state it last confirmed with Beam, which it refreshes every second.

SituationWhat the host does
The webhook is removed or moved to another host, loses subscribe on the channel, or the channel or Room is closedStops at once: sends nothing more, cancels a request in flight, drops the queued messages, deletes the channel's kept messages and dead letters within 5 seconds, and stops polling for its replies. A removed or moved webhook's URL, secret, keys and spool are deleted from the host.
The channel no longer allows receiver_localDeletes the channel's kept messages and dead letters, and delivers it live.
The host cannot confirm the webhook's access, for example while it cannot reach Beam or its lease has expiredHolds the messages and polls nothing, then resumes as soon as access is confirmed again.

None of these counts as a failed delivery or changes the webhook's state. Within about a second of losing access, the webhook receives nothing more. Without durable delivery, held messages wait in the host's queue: while it is full, new messages are refused with webhook_queue_full, and messages still held when the host's agent stops are not delivered. Deleted messages are removed from the spool; their encrypted bytes can remain in freed database pages and free disk space on the host until they are reused.

Members that must upgrade

A webhook gets a channel's keys only from the member whose agent runs that channel's encryption, one of the members holding manage on it. Agents released before webhook members never hand keys to a webhook, so every member that manages a channel the webhook publishes or subscribes to must run an up-to-date agent. Older agents still join Rooms as before.

While such a member manages one of those channels, adding the webhook, accepting it, and granting it publish or subscribe on another channel fail with webhook_controller_upgrade_required, naming the members. A member that gets manage after the webhook was added is not refused. show then prints, without changing the state:

Blocked: member btr_member_... runs an agent without webhook support and must upgrade beam.

list and beam webhook hosted print a Blocked webhook ... line under the table, and with --json the webhook lists the members in upgrade_required_member_ids. The line disappears once they run beam update or lose manage on the webhook's channels.


Bridging Rooms

Bridge between Rooms. One service is a webhook in two Rooms and relays between them. Re-publishing at the delivery's hop plus one lets the hop limit stop the loop after two crossings.

A webhook member belongs to one Room, and its replies go back to that Room. To carry messages from one Room to another, point a webhook at a service that is also a member of the other Room, through a Beam agent, the CLI or an SDK, and have it publish what it receives there. The same service can be a webhook in several Rooms, one webhook member per Room with its own secret, and tell them apart by X-Beam-Room. An agent that is a member of both Rooms can also relay messages itself, without a webhook.

Carry the hop across Rooms

A message the bridge publishes into the other Room is a new message. Published at hop 0, it escapes the hop limit, and a loop between two bridged Rooms can stay under the rate limits: in a test on production, one message bounced between two Rooms 16 times in 21 seconds. Publish it at the delivery's X-Beam-Hop plus one, and the hop limit stops the loop.

  • Carry the hop. Publish each forwarded message with --hop set to the delivery's X-Beam-Hop plus one (hop in the agent's local publish API). A message at hop 2 or more reaches no webhook, so a message crosses between the Rooms at most twice. --hop takes 0 to 16 and publishes live only.

    beam room <other-room-id> channel <channel-id> publish --stdin --hop $((HOP + 1))
  • Skip your own messages. The hop limit still lets the bridge's own forward come back once. Drop every event whose X-Beam-Publisher is the bridge's own member in that Room to avoid that echo.

  • Mark what you forward. If the bridge publishes as a different member than the one it skips, add a marker to forwarded content and drop events that carry it.

  • Plaintext at the bridge. The host decrypts each message before it posts it, so the bridge receives the content in plaintext, like any webhook endpoint, and encrypts it again when it publishes into the other Room. Everything a bridge relays is visible to the bridge and to the webhook's host; Beam's infrastructure still sees only ciphertext.

  • Files. Only files up to the webhook's cap, 25 MiB at most, arrive with their bytes. A larger file arrives as a file.notice, which a bridge cannot forward; relay large files with an agent that is a member of both Rooms.

  • Live only. Nothing is queued while the webhook's host is offline, so a message published then never reaches the bridge.


Manage a webhook

beam room <room-id> webhook resume <webhook-id>
beam room <room-id> webhook move <webhook-id> --host <other-member-id>
beam room <room-id> webhook rotate-secret <webhook-id> --secret-file new.secret --overlap 24h
beam room <room-id> webhook durable <webhook-id> on
beam room <room-id> webhook reply-window <webhook-id> 6h
beam room <room-id> webhook remove <webhook-id>
  • Resume returns a paused or degraded webhook to active. A Room owner or admin can resume it, and so can its host. A webhook without a host cannot be resumed: move it first.
  • Move names a new host. The webhook returns to pending, its channels re-key so the previous host cannot read what is sent afterwards, and the new host accepts it with the URL and secret again. Moving is also how an accepted webhook gets a new URL.
  • Rotate the secret on the host. For the --overlap (default 24h, at most 168h; 0 ends it at once), every request is signed with both the new and the previous secret, so the receiver can switch to the new secret without rejecting requests.
  • Durable and reply-window run on the host: see Durable delivery and Asynchronous replies.
  • Remove revokes the webhook member and re-keys its channels.

When a host leaves the Room or is removed, its webhooks are paused with reason host_left or host_removed and lose their host. Move them to a new host to deliver again.


Troubleshooting

beam room explains the refusals it recognises and prints a hint. With --json, the error carries a stable kind:

KindCauseWhat to do
webhook_manage_unavailableThis machine's agent, or Beam's coordinator, does not support webhook members.beam update, then beam agent restart.
webhook_host_unavailableThis machine's agent cannot host webhooks: it is too old, not connected to a Beam account, or Studio-managed, or Beam's coordinator lacks support.beam update, or have a Room owner move the webhook to another member.
webhook_host_requires_account_agentThis machine joined through an invitation only, so it cannot host.A Room owner moves the webhook to a member connected to a Beam account.
webhook_host_ineligibleThe named host is not an active agent member connected to a Beam account, or its agent lacks hosting support.Name another member.
webhook_permission_deniedAdding, moving, removing, or resuming the webhook needs the Room owner or admin role.Ask a Room owner.
webhook_host_permission_deniedThis machine's agent is not the webhook's host.Run host commands on the host's machine.
webhook_grant_not_allowedA --channel grant is outside the webhook limits.Grant only discover, subscribe, and publish on message channels, and discover and subscribe on object channels.
webhook_channel_requires_retentionA granted message channel does not allow retention none.Allow none on the channel, or leave it out.
webhook_controller_upgrade_requiredA member that manages one of the webhook's channels runs an agent without webhook support. The message names the members.Those members run beam update, or lose manage on the webhook's channels.
webhook_limit_reachedThe Room already holds 10 webhooks, or the host agent 20.Remove one, or name another host.
webhook_state_conflictThe webhook's state does not allow the change, such as resuming a webhook without a host.show it, then move or resume.
webhook_not_found, webhook_target_not_foundThe webhook, Room, host member, or channel was not found.Check the IDs with webhook list, member list, and channel list.
webhook_not_hosted, webhook_not_pending, webhook_not_acceptedA host command named a webhook this machine is not asked to host, has already accepted, or has not accepted.Check beam webhook hosted.
webhook_invalid_requestThe URL, secret, payload mode, file cap, or overlap was refused, or the URL's host is blocked.Follow the hint; a private address needs --allow-private.
webhook_secret_invalidThe secret file is missing or readable by other users, or the secret is not 16 to 512 bytes.chmod 600 the file, or fix the secret.
webhook_test_failedThe test event did not reach the receiver successfully.Follow the hint for the response class.

Next steps

Decentralized distributed bandwidth infrastructure.

On this page