← Back to API docs

Docs

Channel API

The Channel API is realtime pub/sub over WebSockets. Servers publish messages over HTTP; clients subscribe over a single WebSocket that can carry many channels. Clients authenticate with short-lived, channel-scoped tokens, so a browser subscribes without ever holding your API key. A channel with no open connections costs nothing.

How it fits together

  1. Your server mints a subscriber token for the channels a client may use (POST /tokens).
  2. The client opens a WebSocket with that token and receives messages (GET /subscribe).
  3. Your server (or a publish-capable client) publishes a message, which fans out to every open subscriber (POST /publish).

Authentication and base URL

Send requests to https://api.altengine.net. The /tokens, /publish and /presence endpoints authenticate with an organization API key (bearer token); /tokens and /presence also accept an end user's id_token from an Auth instance (see Tokens for end users). /subscribe authenticates with a subscriber token instead. Every path is scoped to a channel instance you provision in the console: /v1/channel/{instance}/….

Channel API endpoints
Method & pathAuthPurpose
POST /v1/channel/{instance}/tokensAPI key (read; write for publish tokens) or an end user's id_tokenMint a subscriber token.
POST /v1/channel/{instance}/publishAPI key (write) or a publish tokenFan a message out to a channel.
GET /v1/channel/{instance}/subscribeSubscriber tokenOpen a WebSocket to receive messages.
GET /v1/channel/{instance}/presenceAPI key (read) or an end user's id_tokenRead a channel's presence roster.

Channel requests are rate-limited per instance, with separate budgets. HTTP publishes (and presence reads) get a sustained 300 per second (18,000 per minute) by default; token mints and new subscribe connections share a 200 per second (12,000 per minute) budget with a large burst to absorb reconnect storms. (WebSocket publishes are governed by their own per-connection cap instead.) Both are separate from the Search API's budget. You can set a lower limit per instance for each of these two budgets in the dashboard (Channels → your app → Rate limit); a configured value can only reduce the rate below the plan maximum, never raise it. Fan-out to subscribers is not rate-limited — only the publish call is — so a single publish reaching thousands of subscribers counts once. Over a limit, a request returns 429 with a RATE_LIMITED code; back off and retry.

Mint a subscriber token

Mint a token server-side and hand it to your client. A token declares the channels it may use and expires after ttl_seconds (default 3600). A value above 14400 is clamped to 14400, not rejected; zero, a negative number, or a non-number returns 400. Minting a subscribe-only token needs a read grant; minting a publish-capable token requires a write grant. The publish field controls which transport(s) the token may publish over:

publish field values
publishToken may publish
false (or omitted)Not at all — subscribe only.
"http"Over POST /publish only. Best for server-side publishing.
"ws"Over its WebSocket only (see Publish over WebSocket). Best for browser clients — it is rate-capped per connection.
"all"Both transports.
trueAlias for "all".
curl -X POST https://api.altengine.net/v1/channel/live/tokens \
  -H "Authorization: Bearer $ALTENGINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channels": ["room-42"], "ttl_seconds": 3600, "publish": false }'
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
  "expires_at": 1752345600,
  "channels": ["room-42"],
  "publish": false,
  "ws_endpoint": "wss://api.altengine.net/v1/channel/live/subscribe",
  "protocols": ["altengine.v1", "altengine.token.eyJhbG…"],
  "ws_url": "wss://api.altengine.net/v1/channel/live/subscribe?token=eyJhbG…"
}
Token response fields
FieldMeaning
tokenThe subscriber token. Safe to hand to a browser.
expires_atExpiry, in seconds since the epoch.
channelsThe channels the token may use.
publishWhere it may publish, or false.
ws_endpointThe WebSocket URL, without the token.
protocolsThe subprotocols to open it with; the second carries the token. See Token auth.
ws_urlDeprecated. The WebSocket URL with the token in its query string.
presence_idThe presence identity, when one was set.

Pass presence_id (printable ASCII, up to 128 bytes) to name this subscriber on the channel's presence roster, for example "user:alice"; the response echoes it. Omitted or empty, the subscriber is anonymous.

Tokens for end users

A browser signed in to an Auth instance mints its own subscriber token: call POST /tokens with the user's id_token as the bearer token instead of an API key. The auth instance's access rules for this channel instance decide which channels it may ask for.

How an end user's subscriber token differs
RuleBehavior
ChannelsRequired, and each must match the user's channel patterns. Otherwise 403 PERMISSION_DENIED.
PublishSubscribe-only. Any publish value returns 403.
Presence identityAlways the user's uid; a presence_id in the request is ignored.
Expiryexpires_at is never later than the id_token's own expiry, whatever ttl_seconds asks for.
RevocationOnce the account is disabled, deleted, has its password reset, or is signed out everywhere (a function's env.auth.revokeSessions), /subscribe refuses the token with 401 (within about a minute). A socket already open stays open until expires_at.
SocketsAt most 20 per user per channel. See Subscribe over WebSocket.

Publish over HTTP

Publish a JSON data payload to one channel. Authenticate with an API key that has a write grant, or with a subscriber token minted with publish of "http" or "all" for that channel. The response reports how many open subscribers received it. (Browser clients can also publish over their socket — see Publish over WebSocket.)

curl -X POST https://api.altengine.net/v1/channel/live/publish \
  -H "Authorization: Bearer $ALTENGINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "room-42", "data": { "text": "hello" } }'

# → { "delivered": 3 }

Each subscriber receives the message as a JSON frame carrying the channel, your payload, and a server timestamp (milliseconds since the epoch):

{ "channel": "room-42", "data": { "text": "hello" }, "ts": 1752345600000 }

The 32 KiB message limit applies to this whole frame as UTF-8 JSON — channel and ts included, not just data. Over it, POST /publish returns 400.

Delivery is at-most-once: a message reaches only the subscribers connected at publish time — there is no backlog or replay for clients that are offline or reconnecting. Publishing to a channel with no open connections returns { "delivered": 0 }.

Subscribe over WebSocket

Open a WebSocket on ws_endpoint with the token's protocols:

const r = await (await fetch("/api/channel-token")).json(); // your backend's mint
const ws = new WebSocket(r.ws_endpoint, r.protocols);

The connection subscribes to all of the token's channels by default; add ?channel= (repeatable) or ?channels=a,b to ws_endpoint to start with a subset. Requesting /subscribe without a WebSocket upgrade returns HTTP 426.

Token auth

/subscribe takes the subscriber token in one of these, checked in this order:

Where /subscribe reads the token
SendUse from
Sec-WebSocket-Protocol: altengine.v1, altengine.token.<token>Browsers: new WebSocket(ws_endpoint, protocols). The server answers with altengine.v1. Offering altengine.token.… without altengine.v1 returns 400.
?token=<token>Deprecated; it may stop working after 31 January 2027. Responses carry Deprecation: true, Sunset: Sun, 31 Jan 2027 00:00:00 GMT and a Link to this section. ws_url is this form.
Authorization: Bearer <token>Servers and native clients that can set headers on the upgrade.

While an instance's clients still use ?token=, its page in the console says so, with when it was last seen.

One socket carries many channels. Incoming frames are the message objects shown above, plus presence events when presence is on. You can adjust the live subscription — within the channels the token authorized — by sending control frames:

WebSocket control frames
SendEffect
{ "type": "subscribe", "channels": ["room-7"] }Add channels to this connection.
{ "type": "unsubscribe", "channels": ["room-7"] }Remove channels from this connection.
ping (raw string)Keepalive; the server replies pong. Once a connection has sent ping, send it at least every 15 minutes.

After a subscribe or unsubscribe, the server acknowledges with the connection's current set: { "type": "subscribed", "channels": [ … ] }. A channel a subscribe asked for and did not get — the token does not authorize it, it already has 10,000 subscribers, or, for an end user's token, that user already holds 20 sockets on it — is listed in "refused": [ … ] on the same ack. A ?channel= the token does not authorize fails the upgrade with 403; one refused for capacity at open is reported in a subscribed frame with refused, sent as soon as the socket opens. With nothing refused at open, no frame is sent. The socket is closed automatically when the token expires — with WebSocket close code 4401 — so refresh the token and reconnect before then.

A connection that has sent ping and then goes 15 minutes without one is closed with code 4408; reconnect. A connection that never pings is not affected. The JS SDK pings every 30 seconds.

Publish over WebSocket

A token minted with publish of "ws" or "all" can publish up its own socket instead of calling POST /publish, with no per-message HTTP round-trip. It is rate-capped per connection rather than against the instance's publish budget. Send a publish frame naming a channel the token is authorized for:

ws.send(JSON.stringify({ type: "publish", channel: "room-42", data: { text: "hello" } }));

The message fans out to that channel's subscribers as the same frame an HTTP publish produces ({ "channel", "data", "ts" }), and the server acknowledges to the sender with the delivery count:

{ "type": "published", "channel": "room-42", "delivered": 3 }

If the frame is rejected — the token lacks WS publish, the channel is not authorized, the framed message is over 32 KiB, or the per-connection rate cap (about 10 publishes per second) is exceeded — the server replies with an error frame instead, and the message is not delivered:

{ "type": "error", "error": "publish rate exceeded — slow down" }

WebSocket publish is ephemeral. Like all channel delivery it is at-most-once and never touches your backend, so there is no server-side record of the message. If you need message history, moderation, or validation, publish server-side instead: have the client send to your backend, which records the message and then calls HTTP publish. Reserve WebSocket publish for low-latency ephemeral traffic — chat where history is optional, typing indicators, cursors, and live reactions.

End-to-end example

// Server: mint a token (never expose the API key to the browser)
const res = await fetch("https://api.altengine.net/v1/channel/live/tokens", {
  method: "POST",
  headers: {
    "Authorization": "Bearer " + process.env.ALTENGINE_API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ channels: ["room-42"], ttl_seconds: 3600 })
});
const r = await res.json();

// Browser: open ws_endpoint with the token's protocols
const ws = new WebSocket(r.ws_endpoint, r.protocols);
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  console.log(msg.channel, msg.data, msg.ts);
};

// Adjust channels live (within the token's authorized set)
ws.send(JSON.stringify({ type: "subscribe", channels: ["room-7"] }));

Presence

Presence is off by default. Turn it on per instance in the dashboard (Channels → your app → Settings → Presence). While it is off, presence_id is ignored, GET /presence returns 403 PERMISSION_DENIED, and no presence events are sent. A socket opened while it was off stays anonymous until it reconnects.

curl "https://api.altengine.net/v1/channel/live/presence?channel=room-42" \
  -H "Authorization: Bearer $ALTENGINE_API_KEY"

# → {
#     "channel": "room-42",
#     "occupancy": 5,
#     "member_count": 2,
#     "members": [ { "id": "user:alice", "connections": 3 }, { "id": "user:bob", "connections": 1 } ],
#     "truncated": false
#   }
Presence response fields
FieldMeaning
occupancyOpen sockets on the channel, anonymous ones included.
member_countDistinct presence identities.
membersOne entry per identity, with how many sockets it holds. At most 1,000 entries; there is no paging.
truncatedtrue when member_count is larger than members.

channel is required. An end user's id_token may read presence only for channels its access rules let it subscribe to; anything else is 403. A socket that dropped without a clean close is counted until the next publish to the channel finds it gone.

Subscribers also receive an event when a member's first socket joins the channel and when its last one leaves. Anonymous sockets never produce one, and the joining socket does not receive its own join:

{ "type": "presence", "event": "join", "channel": "room-42", "id": "user:alice", "ts": 1752345600000 }
{ "type": "presence", "event": "leave", "channel": "room-42", "id": "user:alice", "ts": 1752345660000 }

Region

By default a channel runs near whoever first publishes or subscribes to it. To pin it, set the instance's region in the dashboard (Channels → your app → Settings → Region). New sockets and newly created channels follow it; a channel that is already live is not moved.

Limits

Channel API limits
LimitValue
Channels per token100
Subscribers per channel10,000
Sockets per end user per channel20
Presence identity length128 bytes
Members in a presence response1,000
Channel name length200 bytes
Message size32 KiB, measured on the delivered JSON frame
WebSocket publishes per connection~10 per second
Token TTL (default)3600 seconds (1 hour)
Token TTL (max)14400 seconds (4 hours); longer requests are clamped

Channel names must be printable ASCII, may not start with !, and may not use the reserved __*__ form.