WebSocket
Real-time communication with PalcoTimer via WebSocket.
Introduction
PalcoTimer uses WebSocket for real-time synchronization between all connected devices. Any change (play, pause, timer change, message) is propagated instantly.
Connecting
Connect to the WebSocket using the URL:
wss://app.palcotimer.com/ws/{roomId}?output={tipo}&name={nome}| Parameter | Description |
|---|---|
| roomId | Room ID or short_code |
| output | Recommended; required to send control commands. Command types (can control the room): controller, operator, moderator. Read-only types: viewer, embed, agenda, public_agenda. Omitting the parameter is equivalent to viewer: still tolerated today for legacy clients, but deprecated and will stop allowing control. Always send output=controller (or operator/moderator). |
| name | Device name (optional) |
Client → Server Messages
Request state
{
"type": "request_state"
}Ping (keep connection)
{
"type": "ping"
}Playback controls
// Play
{ "type": "control", "action": "play" }
// Pause
{ "type": "control", "action": "pause" }
// Stop
{ "type": "control", "action": "stop" }
// Reset
{ "type": "control", "action": "reset" }
// Next timer
{ "type": "control", "action": "next" }
// Previous timer
{ "type": "control", "action": "previous" }
// Go to specific timer
{ "type": "control", "action": "go_to", "timer_id": "uuid" }
// Add time (ms)
{ "type": "control", "action": "add_time", "value": 60000 }
// Set time (ms)
{ "type": "control", "action": "set_time", "value": 300000 }Display modes
// Toggle blackout
{ "type": "control", "action": "blackout" }
{ "type": "control", "action": "stop_blackout" }
// Toggle flash
{ "type": "control", "action": "flash" }
{ "type": "control", "action": "stop_flash" }
// Toggle focus (message only)
{ "type": "control", "action": "focus" }
{ "type": "control", "action": "stop_focus" }Messages
// Show message
{ "type": "control", "action": "show_message", "timer_id": "message_uuid" }
// Hide message
{ "type": "control", "action": "hide_message" }From the stage to the server
Only accepted from an output=viewer connection, the speaker's screen. From any other output type they are ignored. The output type is set by the server on connection, never by the payload. Neither changes the timer.
{ "type": "ack_message", "message_id": "message_uuid" }
{ "type": "send_signal", "signal_key": "slide_issue" }ack_message: the speaker confirms the message on display. message_id is the message id; an invalid id, one repeated within the same display, a message that is not on air, or ack_enabled off is silently discarded.
send_signal: triggers a predefined signal by its key (signal_key). At most 1 signal every 3 seconds per connection.
Server → Client Messages
Every server frame has a type field plus the payload fields at the same level. The table below is the list of the events that always exist, with no server feature to turn on, plus the control room-stage communication ones, which only exist when the stage is switched on in the room and say so in their own description. A ? in the payload marks a field that may be absent.
| Event | Recipients | Payload | Description |
|---|---|---|---|
| room | Only the connection involved | { room, timers, messages, playback, server_time } | Full room state. Sent on connection and on every request_state. |
| playback_status | Whole room | { state, timer_id, elapsed_ms, duration_ms, server_time, blackout, flash, focus } | Timer state. Sent on every change and periodically while it runs. |
| timer_update | Whole room | { timer: { action, timer } } | A timer was created or edited. |
| timer_delete | Whole room | { timer: { action, id } } | A timer was deleted. |
| message | Whole room | { message: { action, message? } } | A message was created (action create) or all of them were hidden (action hide, with no body). |
| message_update | Whole room | { message: { action, message } } | A message was edited, shown or hidden. |
| message_delete | Whole room | { message: { action, id } } | A message was deleted. |
| flash | Whole room | { flash } | The stage screen flash was turned on or off. |
| blackout | Whole room | { blackout } | The stage screen blackout was turned on or off. |
| focus | Whole room | { focus } | Focus mode (only the message on the stage screen) was turned on or off. |
| clients | Whole room | { clients? } | The list of room connections changed. |
| schedule_update | Whole room | { delay_ms, original_end?, projected_end?, timers } | Timer times were recomputed: carries the accumulated delay and the timers with their new times. |
| pong | Only the connection involved | { server_time } | Answer to ping, with the server clock. Use it to compute the offset from the local clock. |
| force_reload | Whole room | { } | Request to reload the page. No payload. |
| error | Only the connection involved | { code?, message?, error? } | A command was refused or the connection was not accepted. See the warning below about the format. |
| kicked | Only the connection involved | { message } | The connection was ended by the administrator. It arrives immediately before the socket closes. |
| demo_reset | Whole room | { roomId, at } | The demo room was reset: reload messages and questions. The only event with a camelCase field (roomId). |
| message_ack | Only controller and operator, plus the viewer that sent the action. Never other viewers, embeds or agenda screens | { message_id, conn_id, device_name, acked_at } | Conditional (ack_enabled in the room): the speaker confirmed the message on air ("seen"). Only with ack_enabled in the room. |
| signal | Only controller and operator, plus the viewer that sent the action. Never other viewers, embeds or agenda screens | { signal } | Conditional (signals on in the room): a new stage signal. The label is the one registered in the room, never text sent by the client. |
| signal_update | Only controller and operator, plus the viewer that sent the action. Never other viewers, embeds or agenda screens | { signal } | Conditional (signals on in the room): the control room handled the signal. The viewer that sent it receives the same event, to show "received". |
| sponsors_updated | The stage screen (viewer and embed), the public agenda and the controller. Never other screens | { sponsors: { enabled, blackout, idle, idle_delay_s, rotation_s, style, public_agenda, questions, label, sponsors } } | The room's sponsor setup changed (roster, order or settings). The sponsors field carries the WHOLE configuration: enabled, blackout, idle, idle_delay_s, rotation_s, style, public_agenda, questions, label and the sponsors list of active sponsors (id, name, logo_url, optional link_url and weight). It never carries tier, order or dates. |
| event_ranking_updated | Every connection of the event rooms (stage screen, embed and PWA), through the event channel. It only arrives if the room is attached to an event | { event_id, top: [{ rank, display_label, points }], frozen, updated_at } | Conditional (event channel): the overall event ranking changed. It arrives at most once per second per event, on every connection of the event rooms. It carries event_id, top (up to 50 positions with rank, display_label and points), frozen and updated_at. The display_label is the nickname or an anonymous label; never the real name without the participant consent, nor an e-mail, nor a person identifier. When frozen (frozen true) the big screen keeps the last ranking. |
Room state
{
"type": "room",
"room": { "id": "...", "name": "...", ... },
"timers": [ ... ],
"messages": [ ... ],
"playback": {
"state": "running",
"current_timer_id": "uuid",
"elapsed_ms": 60000,
"blackout": false,
"flash": false,
"focus": false
},
"server_time": 1704067200000
}Playback status
{
"type": "playback_status",
"state": "running",
"timer_id": "uuid",
"elapsed_ms": 65000,
"duration_ms": 300000,
"server_time": 1704067200000,
"blackout": false,
"flash": false,
"focus": false
}Timer update
{
"type": "timer_update",
"timer": {
"action": "update",
"timer": { ... }
}
}Pong
{
"type": "pong",
"server_time": 1704067200000
}The remaining events
Format of the events that have no example of their own above.
// Timer deleted: the id comes, not the timer
{ "type": "timer_delete", "timer": { "action": "delete", "id": "uuid" } }
// Message created, edited and deleted
{ "type": "message", "message": { "action": "create", "message": { ... } } }
{ "type": "message_update", "message": { "action": "update", "message": { ... } } }
{ "type": "message_delete", "message": { "action": "delete", "id": "uuid" } }
// All messages hidden at once: action hide, with no body
{ "type": "message", "message": { "action": "hide" } }
// Stage screen modes (the value is the state AFTER the change)
{ "type": "flash", "flash": true }
{ "type": "blackout", "blackout": false }
{ "type": "focus", "focus": true }
// List of room connections; clients is absent when the list is empty
{
"type": "clients",
"clients": [
{ "id": "uuid", "room_id": "uuid", "output_type": "viewer", "device_name": "Telao 1" }
]
}
// Timer schedule recomputed after a delay
{
"type": "schedule_update",
"delay_ms": 300000,
"original_end": "2026-01-01T12:30:00Z", // optional
"projected_end": "2026-01-01T12:35:00Z", // optional
"timers": [ ... ]
}
// Request to reload the page, for the whole room
{ "type": "force_reload" }
// Error; the text comes in message or in error, depending on the case
{ "type": "error", "code": "FORBIDDEN", "message": "..." }
{ "type": "error", "code": "CONNECTION_LIMIT", "message": "..." }
{ "type": "error", "code": "UNKNOWN_ACTION", "error": "..." }
// Only to the connection the administrator disconnected, right before closing
{ "type": "kicked", "message": "..." }
// Only in the demo room, when it is reset
{ "type": "demo_reset", "roomId": "uuid", "at": 1704067200000 }Polls
Live polls (multiple choice, scale and word cloud) use the same room WebSocket. The poll_* events below are additive: a client that does not know them can safely ignore them, as it already does with any unknown message type.
New fields in the room state
The room message sent on connection gains optional fields. Every field that already existed keeps its name and format.
| Field | Description |
|---|---|
| room.capabilities | Inside room. An object with the capabilities turned on in the server; only true keys appear (today polls and participant_identity) and the whole object is omitted when nothing is on. Absent means off. Same format as GET /rooms/{id}. |
| audience_stage | What occupies the audience content slot on the stage screen: none or poll. May be absent; treat absent as none. |
| poll | The poll currently on the stage screen. Absent when there is none. |
| results | The aggregated result of the poll in poll. Absent when no poll is on the stage screen. |
{
"type": "room",
"room": { "id": "...", "name": "...", ..., "capabilities": { "polls": true } },
"timers": [ ... ],
"messages": [ ... ],
"playback": { ... },
"server_time": 1704067200000,
"audience_stage": "poll",
"poll": { "id": "uuid", "question": "...", "type": "multiple_choice", "status": "open", ... },
"results": { "poll_id": "uuid", "status": "open", "total_votes": 12, "total_voters": 12, ... }
}Poll events
All of them have the type field plus the payload fields at the same level, like the other events.
| Event | Recipients | Payload | Description |
|---|---|---|---|
| poll_opened | Whole room | { poll } | Voting was opened (or reopened). |
| poll_closed | Whole room; the numbers only in some cases (see below) | { poll, results? } | Voting was closed. Carries the final result according to the reach rule below. |
| poll_results | Whole room if the poll is on the stage screen; otherwise only controller and moderator | { results } | Updated result after new votes. Batched: at most one message every 500 ms per poll. |
| poll_shown | Whole room | { poll, results, audience_stage } | The poll went to the stage screen. Carries the current result (may be null if it could not be computed) and the new audience_stage. |
| poll_hidden | Whole room | { poll_id, audience_stage } | The poll left the stage screen. Carries the new audience_stage. The highlighted Q&A question does not come back on its own. |
| poll_votes_reset | Whole room | { poll_id } | Votes were reset. The zeroed result follows in poll_results. |
| poll_deleted | Whole room if the poll was open or on the stage screen; otherwise only controller and moderator | { poll_id } | The poll was deleted. If it was on the stage screen, a poll_hidden is also sent. |
| poll_created | Only controller and moderator | { poll } | A poll (draft) was created. |
| poll_updated | Only controller and moderator | { poll } | A poll was edited. |
| poll_word_pending | Only controller and moderator | { poll_id, vote_id, word } | A word-cloud word is waiting for moderation. Carries the word and the vote identifier. |
Who receives the numbers
The aggregated result (the results field) goes to the whole room only while the poll is on the stage screen, because then the numbers are already on the big screen. Off the stage screen, results goes only to the production screens (controller and moderator). This applies to poll_results, to poll_closed and to the results in the room state.
poll and results format
// poll
{
"id": "uuid", "room_id": "uuid",
"question": "...",
"type": "multiple_choice", // multiple_choice | scale | word_cloud
"options": [{ "id": "...", "text": "..." }], // multiple_choice
"scale": { "min": 1, "max": 5, "min_label": "...", "max_label": "..." }, // scale
"word_cloud": { "max_words_per_voter": 3, "max_word_length": 30 }, // word_cloud
"status": "open", // draft | open | closed
"show_on_stage": true,
"results_visibility": "always", // always | after_vote | when_closed | never
"position": 0,
"created_at": "2026-01-01T12:00:00Z",
"opened_at": "2026-01-01T12:05:00Z", // optional
"closed_at": "2026-01-01T12:10:00Z", // optional
"total_votes": 12
}
// results
{
"poll_id": "uuid", "status": "open",
"total_votes": 12, "total_voters": 12,
"options": [{ "id": "...", "text": "...", "count": 7, "percent": 58.3 }], // multiple_choice
"scale": { "average": 3.8, "distribution": { "1": 0, "2": 1, "3": 4 } }, // scale
"words": [{ "word": "...", "count": 5 }] // word_cloud
}
// audience_stage: "none" | "poll"Live quiz
The live quiz (multiple-choice questions with scoring and a ranking) uses the same room WebSocket. The quiz_* events are additive: a client that does not know them can safely ignore them, as it already does with any unknown message type.
New fields in the room state
While the quiz is on the stage screen, the room message gains the optional quiz field — the same format as the quiz field of quiz_state — and audience_stage also accepts the value quiz. The stage screen has a single audience content slot: a poll and the quiz cannot share it. When the quiz takes it, the poll that was on air leaves with a poll_hidden and the highlighted Q&A question leaves with a question_highlights_cleared.
Quiz events
All of them have the type field plus the payload fields at the same level, like the other events.
| Event | Recipients | Payload | Description |
|---|---|---|---|
| quiz_state | Whole room if the quiz is on the stage screen; otherwise only controller and moderator | { server_time, audience_stage, quiz } | The full quiz state, and the source of truth on reconnect. A null quiz means no quiz is on air. |
| quiz_question_open | Whole room | { server_time, quiz_id, question } | A question was fired. It never carries the answer key. opened_at and deadline are on the server clock. |
| quiz_answer_count | Whole room if the quiz is on the stage screen; otherwise only controller and moderator | { quiz_id, question_id, count } | How many answers have arrived. Batched: at most two messages per second per room. |
| quiz_question_closed | Whole room if the quiz is on the stage screen; otherwise only controller and moderator | { server_time, quiz_id, question_id, correct_option, distribution, total_answers } | The question closed. It is the only event that carries the answer key (correct_option), and only after the close. |
| quiz_ranking | Whole room when the production takes the quiz to the ranking step; in the other cases only controller and moderator | { quiz_id, stage, total_participants, entries } | Ranking of the top places. It is also sent to the production screens on every question close, even with the stage screen on another step. |
| quiz_finished | Whole room if the quiz is on the stage screen; otherwise only controller and moderator | { quiz_id, total_participants, entries } | The quiz ended and the score is frozen. Carries the final ranking. The podium stays on the stage screen until the operator removes it. |
| quiz_hidden | Whole room | { audience_stage } | The quiz left the stage screen. audience_stage is the state of the audience slot AFTER it left. |
quiz, question and entries format
// quiz (the quiz field of quiz_state and of the room state)
{
"id": "uuid", "title": "...",
"status": "running", // draft | running | finished
"stage": "question", // lobby | question | reveal | ranking | podium
"question_index": 2, "question_count": 10, "participant_count": 48,
"question": { ... }, // only on the question step
"reveal": { "question_id": "uuid", "correct_option": 1, "distribution": [3, 9, 1, 0], "total_answers": 13 },
"ranking": [ ... ] // only on the ranking and podium steps
}
// question (no answer key)
{
"id": "uuid", "index": 3, "total": 10,
"text": "...", "options": ["...", "..."],
"time_limit_ms": 30000,
"opened_at": 1704067200000, // epoch ms on the server clock
"deadline": 1704067230000
}
// entries (also the entries field of quiz_ranking and quiz_finished)
[
{ "position": 1, "quiz_participant_id": "uuid", "display_label": "...", "score": 2840, "correct_time_sum_ms": 18230 }
]
// audience_stage: "none" | "poll" | "quiz"Audience questions (Q&A)
The questions the audience sends use the same room WebSocket. There is no environment variable for Q&A: it is turned on in each room's settings (qa_enabled).
Question events
All of them have the type field plus the payload fields at the same level, like the other events.
| Event | Recipients | Payload | Description |
|---|---|---|---|
| question_created | Whole room | { question } | A new question arrived from the audience. With moderation on it arrives with status pending: do not put it on the stage screen before a question_approved. |
| question_approved | Whole room | { question } | The question was approved by moderation. |
| question_rejected | Whole room | { question } | The question was rejected by moderation. |
| question_answered | Whole room | { question } | The question was marked as answered. |
| question_highlighted | Whole room | { question } | The question was highlighted on the stage screen. Only one is highlighted at a time. |
| question_upvoted | Whole room | { question } | The question got a vote. A repeated vote from the same visitor produces no event. |
| question_deleted | Whole room | { question_id } | The question was deleted. It carries only the identifier. |
| question_highlight_cleared | Whole room | { question_id } | The highlight of that question was removed. |
| question_highlights_cleared | Whole room | { } | Every highlight was removed. No payload. It is also emitted when a poll or the quiz takes the audience slot of the stage screen. |
question format
// question
{
"id": "uuid", "room_id": "uuid",
"text": "...",
"author_name": "...", // optional
"status": "pending", // pending | approved | rejected | answered
"highlighted": false,
"upvotes": 7,
"created_at": "2026-01-01T12:00:00Z",
"reviewed_at": "2026-01-01T12:01:00Z", // optional
"answered_at": "2026-01-01T12:05:00Z" // optional
}Enriched agenda
The enriched public agenda (speakers, tracks, locations and the event map) uses the same room WebSocket, so that the phone the audience has open reflects an edit made by the production without reloading the page.
New field in the room state
On the connections that draw the agenda (output=public_agenda and output=agenda), the room message gains the optional agenda field, with exactly the same content as agenda_update, and room.capabilities.agenda_enriched comes back true. On the other output types the frame is identical to the usual one. The public payload carries no private production notes, no room_id and no creation dates.
The agenda_update event
Emitted on every agenda edit made by the production: creating, editing or deleting a speaker, a location or the event map, and saving the public agenda block of a session. It carries the WHOLE agenda, not a delta: replace what you hold in memory with the content you receive.
| Event | Recipients | Payload | Description |
|---|---|---|---|
| agenda_update | Only the output=public_agenda and output=agenda connections | { agenda } | The room agenda changed. The payload is the complete agenda, in the same format it came in on the first frame. |
| agenda_notice | Only the output=public_agenda connections (the audience phone agenda), and only with ENABLE_WEB_PUSH on | { kind, timer_id?, title, body, texts?, created_at } | An agenda notice: schedule change, session cancellation or a manual notice from the operator. It mirrors, on the screen of anyone with the agenda open, the push notification. It carries kind, title, body and created_at, plus timer_id when the notice is about a single session. In automatic notices, texts carries title and body per language (pt-BR and en). It never carries a participant name or identifier. |
agenda fields
| Field | Description |
|---|---|
| agenda.speakers | The room's speaker catalog. An array, never null. |
| agenda.speakers[].id | The speaker identifier. It is the value used in sessions[].speaker_ids. |
| agenda.speakers[].name | Display name. Always present. |
| agenda.speakers[].title_company | Role and company. Absent when not filled in. |
| agenda.speakers[].bio | Short biography. Absent when not filled in. |
| agenda.speakers[].photo_url | https URL of the photo. Absent when not filled in. |
| agenda.locations | The event locations. An array, never null. |
| agenda.locations[].id | The location identifier. It is the value used in sessions[].location_id. |
| agenda.locations[].name | Location name. Always present. |
| agenda.locations[].pin_x | Horizontal position of the pin on the map, from 0 to 1, relative to the image. Absent when the location has no pin. |
| agenda.locations[].pin_y | Vertical position of the pin on the map, from 0 to 1, relative to the image. Absent when the location has no pin. |
| agenda.sessions | The public agenda block of the sessions. An array, never null. It only carries a session with something filled in: an empty session does not show up. |
| agenda.sessions[].timer_id | Identifier of the timer the session corresponds to. It is the key to match the timers array of the room state. |
| agenda.sessions[].description | Public description of the session. Absent when not filled in. |
| agenda.sessions[].track | Track of the session. Absent when not filled in. |
| agenda.sessions[].location_id | Identifier of the session location, among those in locations. Absent when the session has no location. |
| agenda.sessions[].speaker_ids | Identifiers of the session speakers, among those in speakers. An array, never null; it may come empty. |
| agenda.event_map | The event map, one per room. Absent when the room has no map. |
| agenda.event_map.image_url | https URL of the map image. The pins in locations are relative to it. |
{
"type": "agenda_update",
"agenda": {
"speakers": [
{
"id": "66666666-6666-4666-8666-666666666666",
"name": "Ana Souza",
"title_company": "CTO, Acme", // optional
"bio": "Fala de plataformas.", // optional
"photo_url": "https://cdn.exemplo/ana.jpg" // optional
}
],
"locations": [
{
"id": "77777777-7777-4777-8777-777777777777",
"name": "Auditorio",
"pin_x": 0.25, // optional
"pin_y": 0.75 // optional
}
],
"sessions": [
{
"timer_id": "22222222-2222-4222-8222-222222222222",
"description": "Como escalamos o palco.", // optional
"track": "Engenharia", // optional
"location_id": "77777777-7777-4777-8777-777777777777", // optional
"speaker_ids": ["66666666-6666-4666-8666-666666666666"]
}
],
"event_map": { "image_url": "https://cdn.exemplo/planta.png" } // optional
}
}A room with no agenda data
A room that never filled the agenda in — or that had everything deleted — receives the same event, with the three arrays empty and no event_map. This is the minimum payload:
{
"type": "agenda_update",
"agenda": {
"speakers": [],
"locations": [],
"sessions": []
}
}Control room and stage communication
The speaker, on a viewer screen on stage, can confirm they saw the message on air ("seen") and trigger predefined signals, such as "slide problem". The control room receives both in real time over the same room WebSocket.
New fields in the room state
Only on controller and operator connections, and only with the feature on, the type room message gains two optional fields: message_acks (the "seen" confirmations of the current cycle of the message on air, only with ack_enabled) and signals (the signals of the last 24 hours, most recent first, only with enabled in the room). They are absent when empty. room.capabilities.stage_signals is true. No existing field changes; viewer, embed and agenda receive the usual frame.
{
"type": "room",
"room": { "capabilities": { "stage_signals": true } },
"message_acks": [
{
"message_id": "aaaaaaaa-0000-4000-8000-00000000000a",
"conn_id": "bbbbbbbb-0000-4000-8000-00000000000b",
"device_name": "iPad Palco",
"acked_at": "2026-10-08T20:52:03Z"
}
],
"signals": [
{
"id": "cccccccc-0000-4000-8000-00000000000c",
"signal_key": "slide_issue",
"label": "Problema no slide",
"conn_id": "bbbbbbbb-0000-4000-8000-00000000000b",
"device_name": "iPad Palco",
"status": "new",
"created_at": "2026-10-08T20:52:10Z"
}
]
}Event format
device_name is the name the device reported: at most 60 characters, with no control characters. It is user text: show it as text, never as HTML. conn_id is generated by the server. A signal status is new or acked; acked_at and acked_by_device only exist once handled.
// message_ack
{
"type": "message_ack",
"message_id": "aaaaaaaa-0000-4000-8000-00000000000a",
"conn_id": "bbbbbbbb-0000-4000-8000-00000000000b",
"device_name": "iPad Palco",
"acked_at": "2026-10-08T20:52:03Z"
}
// signal
{
"type": "signal",
"signal": {
"id": "cccccccc-0000-4000-8000-00000000000c",
"signal_key": "slide_issue",
"label": "Problema no slide",
"conn_id": "bbbbbbbb-0000-4000-8000-00000000000b",
"device_name": "iPad Palco",
"status": "new",
"created_at": "2026-10-08T20:52:10Z"
}
}
// signal_update (handled by the control room)
{
"type": "signal_update",
"signal": {
"id": "cccccccc-0000-4000-8000-00000000000c",
"signal_key": "slide_issue",
"label": "Problema no slide",
"conn_id": "bbbbbbbb-0000-4000-8000-00000000000b",
"device_name": "iPad Palco",
"status": "acked",
"created_at": "2026-10-08T20:52:10Z",
"acked_at": "2026-10-08T20:52:40Z", // only once handled
"acked_by_device": "Régie Principal" // only once handled
}
}Errors of this feature
These are type error frames sent ONLY to the connection that sent the signal, never to the room. Identify them by the code field.
// error (to the sender only)
{ "type": "error", "code": "signal_rate_limited", "retry_after_ms": 2100 }
{ "type": "error", "code": "signal_unknown_or_inactive" }signal_rate_limited: the anti-spam refused the signal. retry_after_ms tells how many milliseconds remain before sending again.
signal_unknown_or_inactive: the key does not exist, is inactive, or signals are off in the room. It carries no retry_after_ms.
Complete Example
// JavaScript - Connect and control
const ws = new WebSocket('wss://app.palcotimer.com/ws/ABC123?output=controller');
ws.onopen = () => {
console.log('Connected!');
// Request initial state
ws.send(JSON.stringify({ type: 'request_state' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'room':
console.log('Room state:', data);
break;
case 'playback_status':
console.log('Playback:', data.state, data.elapsed_ms);
break;
}
};
// Control
function play() {
ws.send(JSON.stringify({ type: 'control', action: 'play' }));
}
function pause() {
ws.send(JSON.stringify({ type: 'control', action: 'pause' }));
}