Skip to main content

Channels

Looking for the product view?
The Help Center explains this surface as users see it: Live channels.

Three endpoints. One is the read-only view behind the App Admin panel's Channels tab: an App declares channels and broadcasts to them from its own handlers (see the App Channels guide), and this endpoint reports what it declared, what the server allows, and who is connected right now. The other two are not called over HTTP at all: they are the requests an App's page makes over its channel socket when it calls channel.send() or catches up after a reconnect, listed here so the codes they answer with can be looked up.

GET /api/apps/{id}/channels​

The App's live-channel surface: the server's mode and limits, the manifest relay block, the deployed channels/ handlers, and the counters behind them.

Authentication: Required

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Key Fields:

FieldDescription
enabledfalse when an administrator switched the feature off (app.channels.enabled). Nothing can subscribe, and the counters below are then unreadable (countersAvailable: false), not idle
originModefalse when the server serves Apps from the Informer origin, where nothing can subscribe
limitsThe server's app.channels caps: maxSubscribersPerApp, maxChannelsPerSocket, maxFrameBytes, broadcastRate (perSecond, burst), inboundRate (perSecond, burst; the per-user cap on send() and on the replay reads a reconnect makes), replay (frames, ttlMs; how many frames each channel keeps for reconnecting pages, and for how long), joinTimeoutMs
relayThe channels: block of informer.yaml, sorted by name: name, description, on (the events an emit() also broadcasts; always present, since an entry without it fails the deploy)
handlersThe deployed channels/ files: channel (a hapi-style pattern such as /orders/:region), handlerPath, exports (the lifecycle exports plus every event name the file accepts from send()), roles, and the timeout the handler actually gets
countersAvailablefalse when the counters could not be read. The three blocks below are then zeros rather than measurements, and must not be presented as one
livesubscribers cluster-wide, and byChannel per exact channel name
last60mbroadcasts, deliveries, and rateLimited over the last 60 one-minute buckets
last30djoins, refusals, leaves, and sends (inbound messages handled) from the channel invocation rows

A channel an App broadcasts to without declaring it is real and counted: it appears in live.byChannel with no matching relay or handlers entry.

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps); 404 if the user cannot read the app at all

HAL Relation: inf:app-channels. Its absence means the server predates live channels, and clients should hide the surface rather than call the path. A response without limits.inboundRate, limits.replay, or last30d.sends comes from a server that predates inbound messages and replay; clients leave those rows and tiles out rather than showing zeros.

Socket-only routes​

The two routes below exist for the page-side channel client, which invokes them as requests over the App's channel WebSocket (the same socket that carries its subscriptions). Both are refused over plain HTTP with 403 app_channel_socket_required: there is no socket, so there is nothing to send on and no subscription to read back. There is no API key, bearer, or session that makes them work over HTTP, and an App's server-side code has no reason to call them; the page's channel.send() and its reconnect logic are the only callers.

Both are authorized by the socket's subscription, not by a permission. A request names a channel, and the socket must currently hold that channel (it was admitted by the channel's join handler and config.roles, or the channel is open). A channel the socket does not hold answers 403 app_channel_not_subscribed, whatever else is true of the user. The socket credential also pins the App: a path naming another App than the one the socket was minted for answers 403 app_channel_app_mismatch.

POST /api/apps/{id}/channels/_send​

What channel.send(event, payload) performs. Runs the event-named export of the channels/ handler file covering the channel, as the socket's user, with the message as payload, and answers with the export's return value.

Request body:

FieldTypeDescription
channelstringA channel the socket holds. A wildcard (rooms/*) is never held as a concrete channel and is refused by the page client before the request is made
eventstringThe export to run: a valid event name (letters, digits, _, ., -, at most 64 characters). error and connected are reserved
payloadany JSONThe message. Omitted becomes null. The body is capped at 128 KiB

Response: { "result": <return value> }, with null when the export returned nothing.

Errors:

StatusmessageMeaning
403app_channel_socket_requiredNot invoked over an App channel socket
403app_channel_app_mismatchThe socket was minted for a different App
403app_channel_not_subscribedThe socket does not hold channel
404app_channel_no_handlerThe handler file has no export named event, event names an export with a fixed meaning (config, join, joined, leave), or the channel is open (no channels/ file covers it)
400app_channel_reserved_eventevent is error or connected
400app_channel_invalid_eventevent is not a valid event name
429app_channel_rate_limitedThe user is over limits.inboundRate for this App (a token bucket per user per App, shared by all of that user's tabs). Taken before anything else the route does, so a refused message spends a token like any other
402(budget reason)The App's compute budget for the period is spent
504app_channel_send_timeoutThe export ran past its cap (config.timeout, never above joinTimeoutMs)
500app_channel_send_failedThe export threw

Every run, successful or not, is recorded as a SEND invocation row under channel:<name>#<event> and counted in last30d.sends. A thrown export is also counted for the operator as send_failed, a rate-limited one as send_rate_limited, in the App's log stream under the channel source.

GET /api/apps/{id}/channels/_replay​

What the page client reads after every re-subscribe (and on a first subscribe made with channel(name, { since })): the frames a channel still buffers past a sequence number, so a page that dropped and came back can deliver what it missed before any live frame.

Query Parameters:

ParameterTypeDescription
channelstringA concrete channel the socket holds, or one beneath a wildcard it holds (a socket on rooms/* may read back rooms/east)
sinceinteger, default 0The last seq the page delivered on that channel. 0 asks for everything still buffered

Response:

FieldDescription
framesThe buffered frames with seq greater than since, oldest first, each the full envelope a subscriber would have received (tenant, appId, channel, event, payload, seq, at). When the channel's counter has restarted below since, every buffered frame is returned instead
oldestThe smallest seq still buffered, or null when nothing is
currentThe channel's current seq. 0 means the counter is not there: before the channel's first frame, or after it restarted (idle long enough to expire, Redis flushed, or a failover to a replica that never had it). Either way current below since is a restart, and the caller must not keep measuring against the old numbering

The page reads the three together: current === since means nothing was missed; oldest <= since + 1 means every missed frame is in frames; otherwise (including oldest: null with current > since, an emptied buffer) frames were sent that the buffer no longer reaches back to, and the page reports replay_gap so the App can refetch that channel's state. The check errs toward reporting: a frame broadcast with replay: false takes a seq without being buffered, so a page away longer than the window while only such frames flowed is told replay_gap too. The buffer holds at most limits.replay.frames frames, for limits.replay.ttlMs milliseconds after its last write, per channel. Asking for a wildcard name itself answers { "frames": [], "oldest": null, "current": 0 }: a wildcard has no buffer of its own.

Errors:

StatusmessageMeaning
403app_channel_socket_requiredNot invoked over an App channel socket
403app_channel_app_mismatchThe socket was minted for a different App
403app_channel_not_subscribedThe socket holds neither channel nor a wildcard above it
429app_channel_rate_limitedThe user is over limits.inboundRate for this App: replay reads spend from the same per-user bucket as send(), taken before the App lookup. A reconnecting page reads once per concrete channel it holds, so a wildcard that heard from more channels than burst sees some reads refused; the page client reports each as rate_limited with err.channel and still replays the others. Counted for the operator as replay_rate_limited
400(validation message)channel is not a valid channel name, or since is not a non-negative integer

Errors:

StatusmessageMeaning
403app_channel_socket_requiredNot invoked over an App channel socket
403app_channel_app_mismatchThe socket was minted for a different App
403app_channel_not_subscribedThe socket holds neither channel nor a wildcard above it
400(validation)channel is not a valid channel name, or since is not a non-negative integer