WebSocket
Comunicação em tempo real com o PalcoTimer via WebSocket.
Introdução
O PalcoTimer usa WebSocket para sincronização em tempo real entre todos os dispositivos conectados. Qualquer alteração (play, pause, mudança de timer, mensagem) é propagada instantaneamente.
Conectando
Conecte ao WebSocket usando a URL:
wss://app.palcotimer.com/ws/{roomId}?output={tipo}&name={nome}| Parâmetro | Descrição |
|---|---|
| roomId | ID ou short_code da sala |
| output | Recomendado; obrigatório para enviar comandos de controle. Tipos de comando (controlam a sala): controller, operator, moderator. Tipos somente leitura: viewer, embed, agenda, public_agenda. Omitir o parâmetro equivale a viewer: hoje ainda é tolerado para clientes antigos, mas está descontinuado e deixará de permitir controle. Informe sempre output=controller (ou operator/moderator). |
| name | Nome do dispositivo (opcional) |
Mensagens Cliente → Servidor
Solicitar estado
{
"type": "request_state"
}Ping (manter conexão)
{
"type": "ping"
}Controles de playback
// Play
{ "type": "control", "action": "play" }
// Pause
{ "type": "control", "action": "pause" }
// Stop
{ "type": "control", "action": "stop" }
// Reset
{ "type": "control", "action": "reset" }
// Próximo timer
{ "type": "control", "action": "next" }
// Timer anterior
{ "type": "control", "action": "previous" }
// Ir para timer específico
{ "type": "control", "action": "go_to", "timer_id": "uuid" }
// Adicionar tempo (ms)
{ "type": "control", "action": "add_time", "value": 60000 }
// Definir tempo (ms)
{ "type": "control", "action": "set_time", "value": 300000 }Modos de exibição
// Ativar/desativar blackout
{ "type": "control", "action": "blackout" }
{ "type": "control", "action": "stop_blackout" }
// Ativar/desativar flash
{ "type": "control", "action": "flash" }
{ "type": "control", "action": "stop_flash" }
// Ativar/desativar focus (apenas mensagem)
{ "type": "control", "action": "focus" }
{ "type": "control", "action": "stop_focus" }Mensagens
// Mostrar mensagem
{ "type": "control", "action": "show_message", "timer_id": "message_uuid" }
// Esconder mensagem
{ "type": "control", "action": "hide_message" }Do palco para o servidor
Só são aceitos de uma conexão output=viewer, a tela do palestrante. De qualquer outro tipo de saída são ignorados. O tipo de saída é definido pelo servidor na conexão, nunca pelo payload. Nenhum dos dois altera o cronômetro.
{ "type": "ack_message", "message_id": "message_uuid" }
{ "type": "send_signal", "signal_key": "slide_issue" }ack_message: o palestrante confirma a mensagem exibida. message_id é o id da mensagem; id inválido, repetido na mesma exibição, de mensagem que não está no ar ou com ack_enabled desligado é descartado em silêncio.
send_signal: aciona um sinal pré-definido pela chave (signal_key). No máximo 1 sinal a cada 3 segundos por conexão.
Mensagens Servidor → Cliente
Todo quadro do servidor tem o campo type e, no mesmo nível, os campos do payload. A tabela abaixo é a lista dos eventos que existem sempre, sem depender de recurso ligado no servidor, mais os de comunicação régie-palco, que só existem quando o palco está ligado na sala e dizem isso na própria descrição. Um ? no payload marca campo que pode vir ausente.
| Evento | Quem recebe | Payload | Descrição |
|---|---|---|---|
| room | Só a conexão envolvida | { room, timers, messages, playback, server_time } | Estado completo da sala. Enviado ao conectar e a cada request_state. |
| playback_status | Sala toda | { state, timer_id, elapsed_ms, duration_ms, server_time, blackout, flash, focus } | Estado do cronômetro. Enviado a cada mudança e periodicamente enquanto roda. |
| timer_update | Sala toda | { timer: { action, timer } } | Um timer foi criado ou editado. |
| timer_delete | Sala toda | { timer: { action, id } } | Um timer foi excluído. |
| message | Sala toda | { message: { action, message? } } | Uma mensagem foi criada (action create) ou todas foram ocultadas (action hide, sem corpo). |
| message_update | Sala toda | { message: { action, message } } | Uma mensagem foi editada, exibida ou oculta. |
| message_delete | Sala toda | { message: { action, id } } | Uma mensagem foi excluída. |
| flash | Sala toda | { flash } | O flash do telão foi ligado ou desligado. |
| blackout | Sala toda | { blackout } | O blackout do telão foi ligado ou desligado. |
| focus | Sala toda | { focus } | O modo foco (só a mensagem no telão) foi ligado ou desligado. |
| clients | Sala toda | { clients? } | A lista de conexões da sala mudou. |
| schedule_update | Sala toda | { delay_ms, original_end?, projected_end?, timers } | Os horários dos timers foram recalculados: traz o atraso acumulado e os timers com os horários novos. |
| pong | Só a conexão envolvida | { server_time } | Resposta ao ping, com o relógio do servidor. Use-o para calcular a diferença em relação ao relógio local. |
| force_reload | Sala toda | { } | Pedido para recarregar a página. Sem payload. |
| error | Só a conexão envolvida | { code?, message?, error? } | Um comando foi recusado ou a conexão não foi aceita. Veja o aviso abaixo sobre o formato. |
| kicked | Só a conexão envolvida | { message } | A conexão foi encerrada pelo administrador. Chega imediatamente antes do fechamento do socket. |
| demo_reset | Sala toda | { roomId, at } | A sala de demonstração foi reiniciada: recarregue mensagens e perguntas. Único evento com campo em camelCase (roomId). |
| message_ack | Só controller e operator, mais o viewer que emitiu a ação. Nunca os demais viewers, embeds nem telas de agenda | { message_id, conn_id, device_name, acked_at } | Condicional (ack_enabled na sala): o palestrante confirmou a mensagem no ar ("visto"). Só com ack_enabled na sala. |
| signal | Só controller e operator, mais o viewer que emitiu a ação. Nunca os demais viewers, embeds nem telas de agenda | { signal } | Condicional (sinais ligados na sala): sinal novo do palco. O rótulo (label) é o cadastrado na sala, nunca texto enviado pelo cliente. |
| signal_update | Só controller e operator, mais o viewer que emitiu a ação. Nunca os demais viewers, embeds nem telas de agenda | { signal } | Condicional (sinais ligados na sala): a régie atendeu o sinal. O viewer que o emitiu recebe o mesmo evento, para mostrar "recebido". |
| sponsors_updated | Telão (viewer e embed), agenda pública e controller. Nunca as demais telas | { sponsors: { enabled, blackout, idle, idle_delay_s, rotation_s, style, public_agenda, questions, label, sponsors } } | A configuração de patrocinador da sala mudou (cadastro, ordem ou ajustes). O campo sponsors traz a configuração INTEIRA: enabled, blackout, idle, idle_delay_s, rotation_s, style, public_agenda, questions, label e a lista sponsors dos patrocinadores ativos (id, name, logo_url, link_url opcional e weight). Nunca leva cota, ordem nem datas. |
| event_ranking_updated | Todas as conexões das salas do evento (telão, embed e PWA), pelo canal do evento. Só chega se a sala estiver associada a um evento | { event_id, top: [{ rank, display_label, points }], frozen, updated_at } | Condicional (canal do evento): o ranking geral do evento mudou. Chega no máximo uma vez por segundo por evento, a todas as conexões das salas do evento. Traz event_id, top (até 50 posições com rank, display_label e points), frozen e updated_at. O display_label é o apelido ou um rótulo anônimo; nunca o nome real sem consentimento do participante, nem e-mail, nem identificador de pessoa. Congelado (frozen verdadeiro), o telão mantém o último ranking. |
Estado da sala
{
"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
}Status de playback
{
"type": "playback_status",
"state": "running",
"timer_id": "uuid",
"elapsed_ms": 65000,
"duration_ms": 300000,
"server_time": 1704067200000,
"blackout": false,
"flash": false,
"focus": false
}Atualização de timer
{
"type": "timer_update",
"timer": {
"action": "update",
"timer": { ... }
}
}Pong
{
"type": "pong",
"server_time": 1704067200000
}Os demais eventos
Formato dos eventos que não têm exemplo próprio acima.
// Timer excluído: vem o id, não o timer
{ "type": "timer_delete", "timer": { "action": "delete", "id": "uuid" } }
// Mensagem criada, editada e excluída
{ "type": "message", "message": { "action": "create", "message": { ... } } }
{ "type": "message_update", "message": { "action": "update", "message": { ... } } }
{ "type": "message_delete", "message": { "action": "delete", "id": "uuid" } }
// Todas as mensagens ocultadas de uma vez: action hide, sem corpo
{ "type": "message", "message": { "action": "hide" } }
// Modos do telão (o valor é o estado DEPOIS da mudança)
{ "type": "flash", "flash": true }
{ "type": "blackout", "blackout": false }
{ "type": "focus", "focus": true }
// Lista de conexões da sala; clients vem ausente quando a lista está vazia
{
"type": "clients",
"clients": [
{ "id": "uuid", "room_id": "uuid", "output_type": "viewer", "device_name": "Telao 1" }
]
}
// Recálculo da agenda de timers depois de um atraso
{
"type": "schedule_update",
"delay_ms": 300000,
"original_end": "2026-01-01T12:30:00Z", // opcional
"projected_end": "2026-01-01T12:35:00Z", // opcional
"timers": [ ... ]
}
// Pedido de recarregar a página, para toda a sala
{ "type": "force_reload" }
// Erro; o texto vem em message ou em error, conforme o caso
{ "type": "error", "code": "FORBIDDEN", "message": "..." }
{ "type": "error", "code": "CONNECTION_LIMIT", "message": "..." }
{ "type": "error", "code": "UNKNOWN_ACTION", "error": "..." }
// Só para a conexão desconectada pelo administrador, logo antes de fechar
{ "type": "kicked", "message": "..." }
// Só na sala de demonstração, quando ela é reiniciada
{ "type": "demo_reset", "roomId": "uuid", "at": 1704067200000 }Enquetes
Enquetes ao vivo (múltipla escolha, escala e nuvem de palavras) usam o mesmo WebSocket da sala. Os eventos poll_* abaixo são aditivos: um cliente que não os conhece pode ignorá-los com segurança, como já faz com qualquer tipo de mensagem desconhecido.
Novos campos no estado da sala
A mensagem type room, enviada ao conectar, ganha campos opcionais. Todos os campos que já existiam mantêm nome e formato.
| Campo | Descrição |
|---|---|
| room.capabilities | Dentro de room. Objeto com as capacidades ligadas no servidor; só aparecem as chaves verdadeiras (hoje polls e participant_identity) e o objeto inteiro é omitido quando nada está ligado. Ausente significa desligado. É o mesmo formato de GET /rooms/{id}. |
| audience_stage | O que ocupa o espaço de conteúdo da plateia no telão: none ou poll. Pode vir ausente; trate ausente como none. |
| poll | A enquete que está no telão neste momento. Ausente quando não há nenhuma. |
| results | O resultado agregado da enquete em poll. Ausente quando não há enquete no telão. |
{
"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, ... }
}Eventos de enquete
Todos têm o campo type mais os campos do payload, no mesmo nível, como os demais eventos.
| Evento | Quem recebe | Payload | Descrição |
|---|---|---|---|
| poll_opened | Sala toda | { poll } | A votação foi aberta (ou reaberta). |
| poll_closed | Sala toda; os números só em alguns casos (veja abaixo) | { poll, results? } | A votação foi encerrada. Leva o resultado final conforme a regra de alcance abaixo. |
| poll_results | Sala toda se a enquete está no telão; senão só controller e moderator | { results } | Resultado atualizado depois de votos novos. Agrupado: no máximo uma mensagem a cada 500 ms por enquete. |
| poll_shown | Sala toda | { poll, results, audience_stage } | A enquete foi para o telão. Traz o resultado atual (pode vir nulo se não foi possível calculá-lo) e o novo audience_stage. |
| poll_hidden | Sala toda | { poll_id, audience_stage } | A enquete saiu do telão. Traz o novo audience_stage. A pergunta em destaque do Q&A não volta sozinha. |
| poll_votes_reset | Sala toda | { poll_id } | Os votos foram zerados. O resultado zerado chega em seguida em poll_results. |
| poll_deleted | Sala toda se a enquete estava aberta ou no telão; senão só controller e moderator | { poll_id } | A enquete foi excluída. Se estava no telão, um poll_hidden também é enviado. |
| poll_created | Só controller e moderator | { poll } | Uma enquete (rascunho) foi criada. |
| poll_updated | Só controller e moderator | { poll } | Uma enquete foi editada. |
| poll_word_pending | Só controller e moderator | { poll_id, vote_id, word } | Uma palavra da nuvem aguarda moderação. Traz a palavra e o identificador do voto. |
Quem recebe os números
O resultado agregado (campo results) vai para a sala toda somente enquanto a enquete está no telão, porque nesse caso os números já estão na tela grande. Fora do telão, results vai apenas para as telas de produção (controller e moderator). Isso vale para poll_results, para poll_closed e para o results do estado da sala.
Formato de poll e results
// 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", // opcional
"closed_at": "2026-01-01T12:10:00Z", // opcional
"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"Quiz ao vivo
O quiz ao vivo (perguntas de múltipla escolha com pontuação e ranking) usa o mesmo WebSocket da sala. Os eventos quiz_* são aditivos: um cliente que não os conhece pode ignorá-los com segurança, como já faz com qualquer tipo de mensagem desconhecido.
Novos campos no estado da sala
Com o quiz no telão, a mensagem type room ganha o campo opcional quiz — o mesmo formato do campo quiz de quiz_state — e audience_stage passa a aceitar o valor quiz. O telão tem um único espaço de conteúdo de plateia: enquete e quiz não convivem. Quando o quiz entra, a enquete que estava no ar sai com um poll_hidden e a pergunta em destaque do Q&A sai com um question_highlights_cleared.
Eventos do quiz
Todos têm o campo type mais os campos do payload, no mesmo nível, como os demais eventos.
| Evento | Quem recebe | Payload | Descrição |
|---|---|---|---|
| quiz_state | Sala toda se o quiz está no telão; senão só controller e moderator | { server_time, audience_stage, quiz } | Estado completo do quiz, e a fonte da verdade na reconexão. quiz nulo significa que nenhum quiz está no ar. |
| quiz_question_open | Sala toda | { server_time, quiz_id, question } | Uma pergunta foi disparada. Nunca traz o gabarito. opened_at e deadline estão no relógio do servidor. |
| quiz_answer_count | Sala toda se o quiz está no telão; senão só controller e moderator | { quiz_id, question_id, count } | Quantas respostas já chegaram. Agrupado: no máximo duas mensagens por segundo por sala. |
| quiz_question_closed | Sala toda se o quiz está no telão; senão só controller e moderator | { server_time, quiz_id, question_id, correct_option, distribution, total_answers } | A pergunta fechou. É o único evento que traz o gabarito (correct_option), e só depois do fechamento. |
| quiz_ranking | Sala toda quando a régie leva o quiz à etapa de ranking; nos demais casos só controller e moderator | { quiz_id, stage, total_participants, entries } | Ranking dos primeiros colocados. Também é enviado às telas de produção a cada fechamento de pergunta, mesmo com o telão em outra etapa. |
| quiz_finished | Sala toda se o quiz está no telão; senão só controller e moderator | { quiz_id, total_participants, entries } | O quiz foi encerrado e a pontuação está congelada. Traz o ranking final. O pódio continua no telão até o operador retirá-lo. |
| quiz_hidden | Sala toda | { audience_stage } | O quiz saiu do telão. audience_stage é o estado do espaço de plateia DEPOIS da saída. |
Formato de quiz, question e entries
// quiz (campo quiz de quiz_state e do estado da sala)
{
"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": { ... }, // só na etapa question
"reveal": { "question_id": "uuid", "correct_option": 1, "distribution": [3, 9, 1, 0], "total_answers": 13 },
"ranking": [ ... ] // só nas etapas ranking e podium
}
// question (sem gabarito)
{
"id": "uuid", "index": 3, "total": 10,
"text": "...", "options": ["...", "..."],
"time_limit_ms": 30000,
"opened_at": 1704067200000, // epoch ms do relógio do servidor
"deadline": 1704067230000
}
// entries (também é o campo entries de quiz_ranking e quiz_finished)
[
{ "position": 1, "quiz_participant_id": "uuid", "display_label": "...", "score": 2840, "correct_time_sum_ms": 18230 }
]
// audience_stage: "none" | "poll" | "quiz"Perguntas da plateia (Q&A)
As perguntas enviadas pela plateia usam o mesmo WebSocket da sala. Não há variável de ambiente para o Q&A: ele é ligado nas configurações de cada sala (qa_enabled).
Eventos de pergunta
Todos têm o campo type mais os campos do payload, no mesmo nível, como os demais eventos.
| Evento | Quem recebe | Payload | Descrição |
|---|---|---|---|
| question_created | Sala toda | { question } | Uma pergunta nova chegou da plateia. Com moderação ligada ela chega com status pending: não a mostre no telão antes de um question_approved. |
| question_approved | Sala toda | { question } | A pergunta foi aprovada pela moderação. |
| question_rejected | Sala toda | { question } | A pergunta foi rejeitada pela moderação. |
| question_answered | Sala toda | { question } | A pergunta foi marcada como respondida. |
| question_highlighted | Sala toda | { question } | A pergunta foi colocada em destaque no telão. Só uma fica em destaque por vez. |
| question_upvoted | Sala toda | { question } | A pergunta recebeu um voto. Voto repetido do mesmo visitante não gera evento. |
| question_deleted | Sala toda | { question_id } | A pergunta foi excluída. Traz só o identificador. |
| question_highlight_cleared | Sala toda | { question_id } | O destaque daquela pergunta foi removido. |
| question_highlights_cleared | Sala toda | { } | Todos os destaques foram removidos. Sem payload. Também é emitido quando uma enquete ou o quiz ocupa o espaço de plateia do telão. |
Formato de question
// question
{
"id": "uuid", "room_id": "uuid",
"text": "...",
"author_name": "...", // opcional
"status": "pending", // pending | approved | rejected | answered
"highlighted": false,
"upvotes": 7,
"created_at": "2026-01-01T12:00:00Z",
"reviewed_at": "2026-01-01T12:01:00Z", // opcional
"answered_at": "2026-01-01T12:05:00Z" // opcional
}Agenda enriquecida
A agenda pública enriquecida (palestrantes, trilhas, locais e mapa do evento) usa o mesmo WebSocket da sala, para o celular aberto da plateia refletir uma edição feita na régie sem recarregar a página.
Novo campo no estado da sala
Nas conexões que desenham a agenda (output=public_agenda e output=agenda), a mensagem type room ganha o campo opcional agenda, com exatamente o mesmo conteúdo de agenda_update, e room.capabilities.agenda_enriched vem verdadeiro. Nos demais tipos de saída o quadro é idêntico ao de sempre. O payload público não leva as notas privadas da régie, nem room_id, nem datas de criação.
O evento agenda_update
Emitido a cada edição da agenda feita na régie: criar, editar ou excluir palestrante, local ou mapa do evento, e gravar o bloco de agenda pública de uma sessão. Leva a agenda INTEIRA, não um delta: substitua o que você tem em memória pelo conteúdo recebido.
| Evento | Quem recebe | Payload | Descrição |
|---|---|---|---|
| agenda_update | Só as conexões output=public_agenda e output=agenda | { agenda } | A agenda da sala mudou. O payload é a agenda completa, no mesmo formato em que ela veio no primeiro quadro. |
| agenda_notice | Só as conexões output=public_agenda (a agenda do celular da plateia), e só com ENABLE_WEB_PUSH ligada | { kind, timer_id?, title, body, texts?, created_at } | Um aviso da agenda: mudança de horário, cancelamento de sessão ou aviso manual do operador. É o espelho, na tela de quem está com a agenda aberta, da notificação push. Leva kind, title, body e created_at, e timer_id quando o aviso é de uma única sessão. Nos avisos automáticos, texts traz título e texto por idioma (pt-BR e en). Nunca leva nome nem identificador de participante. |
Campos de agenda
| Campo | Descrição |
|---|---|
| agenda.speakers | Catálogo de palestrantes da sala. Array, nunca nulo. |
| agenda.speakers[].id | Identificador do palestrante. É o valor usado em sessions[].speaker_ids. |
| agenda.speakers[].name | Nome de exibição. Sempre presente. |
| agenda.speakers[].title_company | Cargo e empresa. Ausente quando não preenchido. |
| agenda.speakers[].bio | Mini-biografia. Ausente quando não preenchida. |
| agenda.speakers[].photo_url | URL https da foto. Ausente quando não preenchida. |
| agenda.locations | Locais do evento. Array, nunca nulo. |
| agenda.locations[].id | Identificador do local. É o valor usado em sessions[].location_id. |
| agenda.locations[].name | Nome do local. Sempre presente. |
| agenda.locations[].pin_x | Posição horizontal do pino no mapa, de 0 a 1, relativa à imagem. Ausente quando o local não tem pino. |
| agenda.locations[].pin_y | Posição vertical do pino no mapa, de 0 a 1, relativa à imagem. Ausente quando o local não tem pino. |
| agenda.sessions | Bloco de agenda pública das sessões. Array, nunca nulo. Só traz sessão com algum campo preenchido: sessão sem nada não aparece. |
| agenda.sessions[].timer_id | Identificador do timer a que a sessão corresponde. É a chave para casar com o array timers do estado da sala. |
| agenda.sessions[].description | Descrição pública da sessão. Ausente quando não preenchida. |
| agenda.sessions[].track | Trilha da sessão. Ausente quando não preenchida. |
| agenda.sessions[].location_id | Identificador do local da sessão, entre os de locations. Ausente quando a sessão não tem local. |
| agenda.sessions[].speaker_ids | Identificadores dos palestrantes da sessão, entre os de speakers. Array, nunca nulo; pode vir vazio. |
| agenda.event_map | Mapa do evento, um por sala. Ausente quando a sala não tem mapa. |
| agenda.event_map.image_url | URL https da imagem do mapa. Os pinos de locations são relativos a ela. |
{
"type": "agenda_update",
"agenda": {
"speakers": [
{
"id": "66666666-6666-4666-8666-666666666666",
"name": "Ana Souza",
"title_company": "CTO, Acme", // opcional
"bio": "Fala de plataformas.", // opcional
"photo_url": "https://cdn.exemplo/ana.jpg" // opcional
}
],
"locations": [
{
"id": "77777777-7777-4777-8777-777777777777",
"name": "Auditorio",
"pin_x": 0.25, // opcional
"pin_y": 0.75 // opcional
}
],
"sessions": [
{
"timer_id": "22222222-2222-4222-8222-222222222222",
"description": "Como escalamos o palco.", // opcional
"track": "Engenharia", // opcional
"location_id": "77777777-7777-4777-8777-777777777777", // opcional
"speaker_ids": ["66666666-6666-4666-8666-666666666666"]
}
],
"event_map": { "image_url": "https://cdn.exemplo/planta.png" } // opcional
}
}Sala sem dados de agenda
Uma sala que nunca preencheu a agenda — ou que teve tudo apagado — recebe o mesmo evento, com os três arrays vazios e sem event_map. Este é o payload mínimo:
{
"type": "agenda_update",
"agenda": {
"speakers": [],
"locations": [],
"sessions": []
}
}Comunicação régie e palco
O palestrante, numa tela viewer no palco, pode confirmar que viu a mensagem no ar ("visto") e acionar sinais pré-definidos, como "problema no slide". A régie recebe os dois em tempo real pelo mesmo WebSocket da sala.
Campos novos no estado da sala
Só nas conexões controller e operator, e só com a função ligada, a mensagem type room ganha dois campos opcionais: message_acks (os "vistos" do ciclo atual da mensagem no ar, só com ack_enabled) e signals (os sinais das últimas 24 horas, os mais recentes primeiro, só com enabled na sala). Ficam ausentes quando vazios. room.capabilities.stage_signals vem verdadeiro. Nenhum campo existente muda; viewer, embed e agenda recebem o quadro de sempre.
{
"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"
}
]
}Formato dos eventos
device_name é o nome que o dispositivo informou: tem no máximo 60 caracteres e não traz caracteres de controle. É texto do usuário: mostre-o como texto, nunca como HTML. conn_id é gerado pelo servidor. O status do sinal é new ou acked; acked_at e acked_by_device só existem depois de atendido.
// 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 (atendido pela régie)
{
"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", // só depois de atendido
"acked_by_device": "Régie Principal" // só depois de atendido
}
}Erros desta função
São quadros type error enviados SÓ à conexão que enviou o sinal, nunca à sala. Identifique-os pelo campo code.
// error (só ao emissor)
{ "type": "error", "code": "signal_rate_limited", "retry_after_ms": 2100 }
{ "type": "error", "code": "signal_unknown_or_inactive" }signal_rate_limited: o anti-spam recusou o sinal. retry_after_ms diz quantos milissegundos faltam para poder enviar de novo.
signal_unknown_or_inactive: a chave não existe, está inativa ou os sinais estão desligados na sala. Não leva retry_after_ms.
Exemplo Completo
// JavaScript - Conectar e controlar
const ws = new WebSocket('wss://app.palcotimer.com/ws/ABC123?output=controller');
ws.onopen = () => {
console.log('Conectado!');
// Solicitar estado inicial
ws.send(JSON.stringify({ type: 'request_state' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'room':
console.log('Estado da sala:', data);
break;
case 'playback_status':
console.log('Playback:', data.state, data.elapsed_ms);
break;
}
};
// Controlar
function play() {
ws.send(JSON.stringify({ type: 'control', action: 'play' }));
}
function pause() {
ws.send(JSON.stringify({ type: 'control', action: 'pause' }));
}