Triggering Events¶
Server-side code triggers events by calling the Pusher HTTP API — either via an official server SDK (which handles auth for you) or by making raw signed HTTP requests.
Pylon imposes these limits on every trigger call:
- Event payload: 10,000 bytes (10 KB) maximum.
- Channels per publish: up to 100 channels in a single
POST /apps/{app_id}/eventscall. - Batch size: up to 10 events in a single
POST /apps/{app_id}/batch_eventscall.
Server SDKs¶
Using an official Pusher server SDK is the recommended approach. The SDK builds and signs every request automatically.
const Pusher = require("pusher");
const pusher = new Pusher({
appId: "<your-app-id>",
key: "<your-app-key>",
secret: "<your-app-secret>",
host: "127.0.0.1",
port: "7000",
useTLS: false,
});
// Trigger an event on a single channel
await pusher.trigger("my-channel", "my-event", { message: "hello" });
// Trigger to multiple channels at once (up to 100)
await pusher.trigger(["ch-1", "ch-2"], "update", { value: 42 });
// Exclude the originating socket (prevents echo)
await pusher.trigger("my-channel", "my-event", { text: "hi" }, {
socket_id: socketId,
});
For TLS-terminated production deployments point host at your proxy and set useTLS: true.
$pusher = new Pusher\Pusher(
'<your-app-key>',
'<your-app-secret>',
'<your-app-id>',
[
'host' => '127.0.0.1',
'port' => 7000,
'scheme' => 'http',
'cluster' => 'mt1',
]
);
// Trigger an event
$pusher->trigger('my-channel', 'my-event', ['message' => 'hello']);
In a Laravel application the broadcaster config in config/broadcasting.php already reads from
the PUSHER_* env vars described on the Connecting Clients page — no extra
Pusher object is needed; use broadcast(new MyEvent()) as usual.
REST API reference¶
Every REST endpoint is authenticated via HMAC-SHA256 signed query parameters. The official SDKs build these automatically; see REST authentication below if you need to sign requests yourself.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
POST |
/apps/{app_id}/events |
Trigger one event on one or more channels |
POST |
/apps/{app_id}/batch_events |
Trigger multiple events in a single request |
GET |
/apps/{app_id}/channels |
List active channels (with optional prefix filter and stats) |
GET |
/apps/{app_id}/channels/{channel_name} |
Get a single channel's state |
GET |
/apps/{app_id}/channels/{channel_name}/users |
List presence members (presence channels only) |
POST |
/apps/{app_id}/users/{user_id}/terminate_connections |
Disconnect all connections for a user |
Error responses¶
Every error is returned as JSON in the shape {"error": "<message>", "status": <code>}.
| Status | When |
|---|---|
400 |
Malformed request — a bad socket_id, an empty or over-long channel list, an unparseable body, an info attribute that is not valid for the channel |
401 |
Authentication failed: bad or missing signature, wrong key, expired timestamp (outside PYLON_REST_AUTH_WINDOW_SECS), bad body_md5, or two query keys that differ only by case |
403 |
The app exists but is disabled |
404 |
Unknown app, or a gated endpoint whose token is not configured |
413 |
The event payload exceeds PYLON_MAX_EVENT_PAYLOAD_BYTES (default 10,000), or the request body exceeds the REST body cap |
503 |
The node is over capacity, in which case the publish is rejected before any broadcast, or the cross-node publish failed, in which case the channels (or batch items) published before the failure were delivered — a retry re-delivers them, so delivery is at-least-once, and on a cache channel the event may already be stored and the retry overwrites it. Either way the response carries Retry-After: 1 so a well-behaved publisher backs off. See below |
503 under load is new in practice
Admission control has always been described, but the node-wide saturation
flag it reads was cleared unconditionally on every worker loop, so this
503 could never actually fire. It now does. A publisher that has been
hammering a node past its memory budget will start seeing
503 {"error":"Server overloaded","status":503} with Retry-After: 1
where it previously got 200 and silently-shed delivery.
Retry on it. The official Pusher SDKs do not retry a 503 for you, so
if your publish path matters, add a backoff that honours Retry-After.
Diagnosis and remedies are in
Troubleshooting — Overload.
POST /apps/{app_id}/events¶
Trigger a single named event on one or more channels.
Request body (JSON):
| Field | Required | Description |
|---|---|---|
name |
yes | Event name (e.g. "order-updated") |
data |
yes | Event payload as a JSON-encoded string; max 10,000 bytes (PYLON_MAX_EVENT_PAYLOAD_BYTES) |
channels |
yes* | Array of channel names (up to 100) |
channel |
yes* | Single channel name (alternative to channels) |
socket_id |
no | Socket ID to exclude from delivery (prevents echo). Must be \d+\.\d+ — two runs of ASCII digits joined by one dot — and at most 24 bytes, else 400 "Invalid socket id" |
info |
no | Comma-separated attributes to return: subscription_count, user_count |
*Provide either channel or channels. If both are sent, channels wins and channel is ignored — this is not an error.
Encrypted channels (private-encrypted-*) must be targeted alone — mixing them with other channels
in one call returns an error.
Server-to-user channels (#server-to-user-<user_id>)¶
Triggering an event on the reserved channel #server-to-user-<user_id> delivers it to every
live connection of that user — the client must have authenticated via pusher:signin. The event
is routed through the user registry (cluster-wide on the Redis adapter), is never cached, and
socket_id exclusion does not apply to it (there is no originating socket among the recipient's
connections).
# Same signing steps as the raw curl example below, with the reserved channel in the body:
BODY='{"name":"invoice-paid","channel":"#server-to-user-42","data":"{\"amount\":99}"}'
BODY_MD5=$(echo -n "$BODY" | md5sum | cut -d' ' -f1)
QUERY="auth_key=${APP_KEY}&auth_timestamp=${TIMESTAMP}&auth_version=1.0&body_md5=${BODY_MD5}"
SIGNATURE=$(echo -en "POST\n/apps/${APP_ID}/events\n${QUERY}" \
| openssl dgst -sha256 -hmac "$APP_SECRET" | cut -d' ' -f2)
curl -s -X POST \
"http://${HOST}/apps/${APP_ID}/events?${QUERY}&auth_signature=${SIGNATURE}" \
-H "Content-Type: application/json" -d "$BODY"
On the client side the signed-in user's connections receive it as an ordinary channel event on
#server-to-user-<user_id> — pusher-js surfaces it through its user-notification handling. A
client may subscribe to its own #server-to-user-<user_id> channel (after signin) and gets a
subscription_succeeded; subscribing to another user's channel — or any other #-prefixed name —
is rejected non-fatally with pusher:subscription_error status 401.
POST /apps/{app_id}/batch_events¶
Trigger up to 10 events in a single request. Each item targets exactly one channel.
Request body (JSON):
{
"batch": [
{
"channel": "my-channel",
"name": "event-a",
"data": "{\"key\":\"value\"}"
},
{
"channel": "presence-room",
"name": "event-b",
"data": "{\"n\":2}",
"socket_id": "123.456"
}
]
}
Each item has the same fields as a single event (channel, name, data, optional socket_id
and info).
Every item's socket_id is validated before any delivery runs, so a single malformed
socket_id anywhere in the batch rejects the whole request with 400 "Invalid socket id" rather
than delivering the earlier items and then failing.
GET /apps/{app_id}/channels¶
List all currently occupied channels for the app.
Query parameters (in addition to auth params):
| Parameter | Description |
|---|---|
filter_by_prefix |
Return only channels whose name starts with this prefix |
info |
Comma-separated attributes: subscription_count, user_count |
user_count is only valid when filter_by_prefix is set to a presence- prefix.
GET /apps/{app_id}/channels/{channel_name}¶
Fetch the state of one channel.
Query parameters (in addition to auth params):
| Parameter | Description |
|---|---|
info |
Comma-separated attributes: subscription_count, user_count, cache |
occupied is not an info attribute — it is returned unconditionally on every response, and
passing info=occupied is silently ignored. cache is valid only on a cache channel (any of the
cache-, private-cache-, presence-cache-, private-encrypted-cache- forms) and returns
{"data": …, "ttl": …}, or null when nothing is cached; requesting it on a non-cache channel is a
400.
GET /apps/{app_id}/channels/{channel_name}/users¶
Return the list of member IDs currently subscribed to a presence channel.
Only works on presence-* channels. Returns {"users": [{"id": "..."}, ...]}.
POST /apps/{app_id}/users/{user_id}/terminate_connections¶
Disconnect all current WebSocket connections for the specified user across the cluster.
Returns {} on success.
REST authentication¶
Every request must carry four query parameters (or five for requests with a body):
| Parameter | Description |
|---|---|
auth_key |
Your app's public key |
auth_timestamp |
Current Unix time in seconds |
auth_version |
Always 1.0 |
body_md5 |
MD5 hex digest of the raw request body — required when a body is present |
auth_signature |
HMAC-SHA256 hex signature (see below) |
Signing string:
Where {sorted-query} is all query parameters except auth_signature, with keys
lowercased and sorted alphabetically, joined as key=value&key=value.
Signature:
In practice the official server SDKs build this automatically — you only need to supply
appId, key, secret, host, and port.
Raw curl example¶
The example below triggers an event without a server SDK. The signature is pre-computed here for illustration — in production, compute it dynamically.
# Variables
APP_ID="my-app-id"
APP_KEY="my-app-key"
APP_SECRET="my-app-secret"
HOST="127.0.0.1:7000"
TIMESTAMP=$(date +%s)
# Request body
BODY='{"name":"order-updated","channel":"orders","data":"{\"id\":99}"}'
# Compute body MD5
BODY_MD5=$(echo -n "$BODY" | md5sum | cut -d' ' -f1)
# Build signing string (params sorted: auth_key, auth_timestamp, auth_version, body_md5)
QUERY="auth_key=${APP_KEY}&auth_timestamp=${TIMESTAMP}&auth_version=1.0&body_md5=${BODY_MD5}"
SIGNING_STRING="POST\n/apps/${APP_ID}/events\n${QUERY}"
# Compute HMAC-SHA256 signature
SIGNATURE=$(echo -en "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$APP_SECRET" | cut -d' ' -f2)
# Send the request
curl -s -X POST \
"http://${HOST}/apps/${APP_ID}/events?${QUERY}&auth_signature=${SIGNATURE}" \
-H "Content-Type: application/json" \
-d "$BODY"