# Publishing from the console

> How the Event Creator publishes an event, what each of its three response states means, and how to reproduce the same publish as a signed REST call.

The Event Creator screen triggers a single event on the selected app and
shows the raw result inline. It is the console equivalent of
`pusher.trigger()`. This page explains what the button actually does,
how to read the response card, and how to turn the form into a signed
REST request that your own server can send.

The screen needs three things before it will publish: a signed-in
session, an org, and a selected app. Without a session it prompts you to
sign in at the portal; without an app it sends you to **Apps & keys**.

## What the console publish does differently from a signed REST call

The console does not sign a REST request. It calls the control-plane
procedure `realtime.publish` (an org-scoped procedure) with
`{ orgId, appId, channel, event, data }`. The control-plane API then
`RPUSH`es the publish body onto the engine's generic BFF-to-engine ring
(`clutchcall:realtime:ring`) — the same bus the presence and
assist bridges ride.

Two consequences follow, and they are the reason console results can
differ from your server's results:

- **No per-deployment HTTP configuration is involved.** The publisher is
  always reachable from the console, because it writes to the ring rather
  than to an engine HTTP endpoint.
- **The control-plane API is an authorized publisher.** `private-*` and
  `presence-*` channels are legitimate targets from this screen, and the
  form never blocks those prefixes. Channel authorization that a browser
  subscriber would have to pass does not apply to the publish side here.

Your own server, by contrast, posts to the engine's Pusher-compatible
REST plane with an HMAC signature. See
[Turning the form into a signed REST request](#turning-the-form-into-a-signed-rest-request).

## Reading the three response states

The **API response** card renders one of three outcomes, driven by the
`ok` and `wired` fields of the procedure result.

| Card heading | Pill | Result shape | Meaning |
| ------------ | ---- | ------------ | ------- |
| Event published | `ok` (live) | `ok: true`, `wired` not `false` | The body was accepted onto the ring and the engine is heartbeating. |
| Queued — engine offline | `queued` | `ok: true`, `wired: false` | The body is on the ring, but the engine is not heartbeating. It is delivered when the engine reconnects. |
| Publish failed | HTTP status, or `error` | `ok: false` | The publish was rejected. The card shows the status when one is present and renders the response body as JSON. |

Before you press **Send event**, the card shows an empty state: "No
response yet."

A fourth outcome is not part of the card at all — see
[Telling a transport failure from a rejected publish](#telling-a-transport-failure-from-a-rejected-publish).

## Queued delivery and what it guarantees

`queued` is a success state, not a failure. The `ok: true` means the
control-plane API wrote your publish body onto the BFF-to-engine ring.
The `wired: false` means it could not observe an engine heartbeat at that
moment. The engine picks the entry up when it reconnects and drains the
ring.

What the state tells you:

- The body was accepted. You do not need to retry the form to get the
  event onto the ring; retrying enqueues a second copy.
- Delivery is deferred, not cancelled.

What the state does **not** tell you:

- **It does not confirm delivery.** Fan-out to channel subscribers
  happens engine-side after the ring is drained. Nothing in this response
  reflects whether any subscriber received the event.
- **It does not carry a hold time or a delivery deadline.** The response
  has no expiry field, and the console does not display one. Treat
  "delivers when the engine reconnects" as the whole of the contract.
- **It is not a replay mechanism.** The ring is a handoff to the engine,
  not subscriber-facing history. Whether a client that connects in the
  interim sees the event depends entirely on when the engine drains the
  ring relative to that client's subscription — the console cannot tell
  you which came first.

If you need to confirm the event actually reached clients, observe it on
a subscriber rather than inferring it from this card.

## Telling a transport failure from a rejected publish

The screen surfaces three distinct classes of failure in three distinct
places. They mean different things:

| Where it appears | Cause | What to do |
| ---------------- | ----- | ---------- |
| Red caption under the form, from the JSON field | `JSON.parse` of the **Data** textarea threw. Text reads `Invalid JSON: <message>`. | Fix the payload. No publish was attempted. |
| Red caption under the buttons, from the mutation error | The `realtime.publish` call itself failed — the tRPC request did not complete or the procedure threw. The text is the tRPC error message. | This is a transport or authorization problem between the console and the control-plane API. The publish body never reached the ring. |
| **Publish failed** in the response card | The procedure completed and returned `ok: false`, with a `status` and a `body`. | This is a *rejected publish*: the request got through and was refused. Read the JSON body in the card for the reason. |

The distinction that matters: a mutation error means "we never got an
answer," while `ok: false` means "we got an answer and it was no." Only
the second one has an HTTP status and a response body to read.

## Channel and event name validation

The form validates shape before it sends anything. Errors appear
underneath the offending input once you have pressed **Send event** at
least once.

**Channel**

- Required.
- Maximum 164 characters.
- Must match `[A-Za-z0-9_\-=@,.;]+` — letters, digits, and
  `_ - = @ , . ;` only.

The character rule is the Pusher protocol channel-name rule. A stock
`pusher-js` client refuses to subscribe to a name outside that set, so
publishing to one is publishing into the void. The control-plane API
enforces the same rule, so bypassing the form does not help.

Channel *prefixes* are deliberately not restricted. `private-orders`
(the default) and `presence-*` names are accepted because the
control-plane API publishes as an authorized publisher.

**Event name**

- Required.
- Maximum 200 characters.

**Data**

Must parse as JSON. The field shows `valid JSON` or `invalid` live as you
type, and **Send event** is disabled while the payload is invalid. The
parsed object — not the raw text — is what goes to the procedure.

## Turning the form into a signed REST request

The **Equivalent cURL** card mirrors your current form values as a
request against the engine's external Pusher-compatible REST plane. This
is the shape your own server SDK sends; it is not the path the console
itself takes.

```bash
# auth_signature = HMAC-SHA256 of the sign-string with your app secret
curl -X POST "https://<ws-host>/apps/<app_id>/events?auth_key=<YOUR_KEY>&auth_timestamp=$(date +%s)&auth_version=1.0&body_md5=<MD5_OF_BODY>&auth_signature=<HMAC>" \
  -H "Content-Type: application/json" \
  -d '{"name":"order.created","channels":["private-orders"],"data":"{\"id\":\"ord_8821\",\"total\":49,\"currency\":\"EUR\"}"}'
```

Points to carry across when you write this by hand:

- **The endpoint is `POST /apps/{app_id}/events`** on the engine host,
  with the app id from the pill at the top of the screen.
- **`data` is a JSON-encoded string, not a nested object.** The console
  re-serializes whatever you typed in the **Data** textarea into a string
  for this snippet. Sending an object instead of a string is the most
  common cause of a rejected publish.
- **`channels` is an array**, even for a single channel.
- **Authentication is query-string HMAC**, not a bearer header:
  `auth_key`, `auth_timestamp`, `auth_version=1.0`, `body_md5`, and
  `auth_signature`. The signature is HMAC-SHA256 of the sign-string with
  your app secret, and `body_md5` is the MD5 of the exact request body
  you send — recompute both if you edit the payload.

Because this path authenticates with the app secret rather than an org
session, its authorization outcome can differ from the console's. A
publish that succeeds from the Event Creator can still be rejected here
if the key or signature is wrong; that arrives as a non-2xx status, the
REST-plane equivalent of the **Publish failed** state above.

## Related

- [Authentication](/concepts/authentication) — API keys and short-lived tokens
- [Telemetry](/platform/telemetry) — where publish-side counters land
