Subscribe to new messages
Open a connection and listen formessage_upsert events. Each event contains a message under data:
WebSocket. The default feed covers all indexed domains and uses the scraper’s canonical confirmation settings.
You receive both newly indexed messages and changes to existing messages. The same msg_id can appear more than once; update the existing entry in your app instead of adding a duplicate.
Watch a single message
Use the same connection and filter bymsg_id in your listener. Replace messageId with the ID copied from the Explorer:
\x instead of 0x; the example converts the prefix for you. Filtering happens in your client: there is no server-side subscription parameter for a single message ID.
The feed sends future updates, not history. If your message was already delivered before you connected, it may not appear. Use the GraphQL message lookup to check its current state; see snapshot and live updates for combining both in an app.
Message updates
Amessage_upsert frame contains an enriched message row under data. For example, this abbreviated frame represents a delivery update; actual rows include additional fields:
data.msg_id to your message, then read data.is_delivered, data.destination_tx_hash, and data.destination_block_height. The same message can be updated multiple times as dispatch, payment, delivery, or transaction details become available. Upsert by msg_id; do not treat every frame as a new message or an exactly-once delivery notification.
Hashes and addresses use the same \x-prefixed bytea encoding as GraphQL. Large integers are decimal strings. See data formats.
Advanced: domains and confirmations
Configure the feed through URL parameters. This is a JSON WebSocket feed, not a GraphQL subscription endpoint.
Use Hyperlane domain IDs, not necessarily native chain IDs. Find them with the GraphQL
domain query. When filtering messages by domain, both confirmations and domains are required. The filter matches messages whose origin or destination is in the list; it does not restrict the feed to a single route between those domains.
For a route-specific view, include both origin and destination domains, then filter received rows by their domain pair. There is no public message-ID or address subscription filter; match those fields in your client.
You can choose your own confirmation count, not just zero. For example, subscribe with five confirmations:
confirmations=N value applies to all requested domains on that connection; it is not a separate count per domain. Custom feeds include data through min(indexed height, head height - N) on each supported domain. Heights use native chain units, such as Sealevel slots. The feed pushes future eligible updates; it does not replay events that were already eligible when you connected.
Omitting confirmations selects canonical scraper finality. See where to find default confirmations for the registry’s per-chain blocks.reorgPeriod settings and how they differ from blocks.confirmations. Check the ready frame below to see which requested domains support your custom delay.
Check the ready frame
After connecting, wait forready. For the Ethereum/Base zero-confirmation example, a successful response can look like:
customDomainsidentifies domains using the requested confirmation delay.canonicalDomainsidentifies requested domains without near-head state; their data retains canonical finality.- These capabilities are negotiated for this connection. Recheck them after reconnecting.
canonicalDomains, the connection is not providing zero-confirmation delivery for that destination. The default canonical feed’s ready frame does not include the custom-confirmation fields.
Canonical frames can omit the top-level confirmations field. WebSocket rows do not provide the GraphQL is_provisional snapshot flag as a guaranteed field. Use the negotiated domain capabilities and a canonical GraphQL lookup when you need to establish canonical delivery.
Handle rollbacks
Custom-confirmation feeds can send:toHeight on the affected domain. This includes origin dispatches and destination deliveries. For a simple message tracker, invalidate the affected message state and reload it through GraphQL. For a list, clearing and reloading the provisional overlay is a straightforward approach. Replacement rows can follow; apply them idempotently.
Do not make is_delivered: true irreversible when it came from provisional data. A subsequent snapshot or replacement row can show the message as undelivered again.
Optional block updates
Addblocks=true to receive a block snapshot after ready, followed by changed heads:
height and hash identify the indexed block; headHeight is the observed chain tip. Heights are decimal strings. Updates can skip heights or move backward after a reorganization, so replace your stored head rather than incrementing it. Block hashes in these frames use 0x encoding.
/messages?blocks=true adds block updates for all available near-head domains to the canonical message feed. With confirmations and domains, block updates are scoped to those domains. Block updates do not wait for the requested message-confirmation delay and are not a full block or transaction stream.
Combine a snapshot with live updates
The public message feed does not replay historical messages or accept a resume cursor. A message delivered before you connected may never produce another frame. To build a delivery tracker:- Open the socket, install the message handler, and wait for
ready. - Buffer incoming updates while fetching the message’s GraphQL snapshot with the same confirmation setting.
- Load the snapshot and reconcile buffered updates by
msg_id. Keep ingestion idempotent. A concurrent snapshot and stream are not an atomic, versioned snapshot; refetch if observations conflict rather than assuming arrival order proves which is newer. - Apply subsequent upserts. If a rollback happens during a snapshot request, discard that in-flight result and fetch again.
- After every reconnect, repeat the snapshot process. Do not assume the stream fills the disconnect gap.
msg_id. Only canonical pages should supply history cursors. Refresh outstanding messages to reconcile delivery changes on older history rows.
Connections and recovery
- Reuse one connection for the domains your application needs. The public service currently permits at most five connections per client IP, including clients sharing an outbound IP.
- Handle both
errorframes ({"type":"error","error":"..."}) and socket closure. Reconnect transient failures with exponential backoff and jitter, then reload snapshots. Correct invalid parameters before retrying. - Process messages promptly. Slow consumers can be disconnected when buffers fill. Use the GraphQL API for historical scans rather than trying to recover history over this feed.
- The server uses WebSocket protocol ping/pong for liveness. Browsers handle these automatically; other clients must support protocol pong responses. An application-level JSON ping is not required for
/messages. - Close the socket when the view or worker no longer needs it. The
/agentsstream is private infrastructure and is not part of this public API.