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âmetroDescrição
roomIdID ou short_code da sala
outputRecomendado; 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).
nameNome 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.

EventoQuem recebePayloadDescrição
roomSó a conexão envolvida{ room, timers, messages, playback, server_time }Estado completo da sala. Enviado ao conectar e a cada request_state.
playback_statusSala 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_updateSala toda{ timer: { action, timer } }Um timer foi criado ou editado.
timer_deleteSala toda{ timer: { action, id } }Um timer foi excluído.
messageSala toda{ message: { action, message? } }Uma mensagem foi criada (action create) ou todas foram ocultadas (action hide, sem corpo).
message_updateSala toda{ message: { action, message } }Uma mensagem foi editada, exibida ou oculta.
message_deleteSala toda{ message: { action, id } }Uma mensagem foi excluída.
flashSala toda{ flash }O flash do telão foi ligado ou desligado.
blackoutSala toda{ blackout }O blackout do telão foi ligado ou desligado.
focusSala toda{ focus }O modo foco (só a mensagem no telão) foi ligado ou desligado.
clientsSala toda{ clients? }A lista de conexões da sala mudou.
schedule_updateSala 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.
pongSó 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_reloadSala toda{ }Pedido para recarregar a página. Sem payload.
errorSó 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.
kickedSó a conexão envolvida{ message }A conexão foi encerrada pelo administrador. Chega imediatamente antes do fechamento do socket.
demo_resetSala toda{ roomId, at }A sala de demonstração foi reiniciada: recarregue mensagens e perguntas. Único evento com campo em camelCase (roomId).
message_ackSó 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.
signalSó 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_updateSó 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_updatedTelã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_updatedTodas 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.
Trate qualquer type desconhecido como ignorável: o protocolo é aditivo e eventos novos aparecem sem aviso. Nunca derrube a conexão nem apresente erro ao operador por causa de um type que você não conhece.

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 }
O envelope de erro não é uniforme
Hoje o texto do erro vem em message nos erros de conexão e de permissão (CONNECTION_LIMIT, FORBIDDEN) e em error no comando desconhecido (UNKNOWN_ACTION). Leia os dois campos. Há ainda um caminho que emite {"type":"error"} sem code nem texto: trate a ausência dos dois sem quebrar.

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.

Só existe com enquetes ligadas
Tudo o que está nesta seção só é enviado quando o recurso de enquetes está ligado no servidor (variável POLLS_ENABLED). Com ele desligado, o protocolo é idêntico ao descrito nas demais seções: nenhum evento poll_* e nenhum dos campos novos do estado da sala. Use room.capabilities.polls para saber se o recurso está ligado.

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.

CampoDescrição
room.capabilitiesDentro 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_stageO que ocupa o espaço de conteúdo da plateia no telão: none ou poll. Pode vir ausente; trate ausente como none.
pollA enquete que está no telão neste momento. Ausente quando não há nenhuma.
resultsO 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.

EventoQuem recebePayloadDescrição
poll_openedSala toda{ poll }A votação foi aberta (ou reaberta).
poll_closedSala 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_resultsSala 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_shownSala 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_hiddenSala 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_resetSala toda{ poll_id }Os votos foram zerados. O resultado zerado chega em seguida em poll_results.
poll_deletedSala 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_createdSó controller e moderator{ poll }Uma enquete (rascunho) foi criada.
poll_updatedSó controller e moderator{ poll }Uma enquete foi editada.
poll_word_pendingSó 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.

Em poll_closed de uma enquete que não está no telão, as telas que não são de produção recebem o evento só com poll, sem results. A plateia lê o resultado pela API HTTP, que respeita a configuração de visibilidade da enquete.

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.

Só existe com o quiz ligado
Tudo o que está nesta seção só é enviado quando o quiz está ligado no servidor (variável QUIZ_ENABLED). Com ele desligado, nenhum evento quiz_* é emitido e o estado da sala não ganha campo novo. Use room.capabilities.quiz para saber se o recurso está ligado.

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.

EventoQuem recebePayloadDescrição
quiz_stateSala 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_openSala 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_countSala 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_closedSala 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_rankingSala 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_finishedSala 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_hiddenSala toda{ audience_stage }O quiz saiu do telão. audience_stage é o estado do espaço de plateia DEPOIS da saída.
O gabarito nunca viaja antes do fechamento: nenhum payload de pergunta aberta tem o campo correct_option. Nenhum evento do quiz carrega nome, e-mail ou identificador de dispositivo — o rótulo que aparece em superfície pública é display_label.

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).

Só a URL com o UUID da sala gera evento
Os eventos question_* só são emitidos quando a rota HTTP que fez a alteração usou o UUID da sala no caminho. A mesma chamada pelo short code altera a pergunta normalmente e responde o mesmo status, mas nenhum evento é enviado pelo WebSocket. É comportamento herdado, mantido de propósito; se a sua integração depende do aviso em tempo real, use o UUID.

Eventos de pergunta

Todos têm o campo type mais os campos do payload, no mesmo nível, como os demais eventos.

EventoQuem recebePayloadDescrição
question_createdSala 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_approvedSala toda{ question }A pergunta foi aprovada pela moderação.
question_rejectedSala toda{ question }A pergunta foi rejeitada pela moderação.
question_answeredSala toda{ question }A pergunta foi marcada como respondida.
question_highlightedSala toda{ question }A pergunta foi colocada em destaque no telão. Só uma fica em destaque por vez.
question_upvotedSala toda{ question }A pergunta recebeu um voto. Voto repetido do mesmo visitante não gera evento.
question_deletedSala toda{ question_id }A pergunta foi excluída. Traz só o identificador.
question_highlight_clearedSala toda{ question_id }O destaque daquela pergunta foi removido.
question_highlights_clearedSala 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.
Todas as telas da sala recebem estes eventos, inclusive o telão e o celular da plateia, e o payload de question_created traz o texto e o author_name mesmo com moderação ligada. Quem exibe é responsável por filtrar por status. O IP e o user agent de quem perguntou nunca aparecem no payload.

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.

Só existe com a agenda enriquecida ligada
Tudo o que está nesta seção só é enviado quando a agenda enriquecida está ligada no servidor (variável AGENDA_ENRICHED_ENABLED). Com ela desligada, nenhum agenda_update é emitido e o estado da sala não leva o campo agenda. Use room.capabilities.agenda_enriched para saber se o recurso está ligado.

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.

EventoQuem recebePayloadDescrição
agenda_updateSó 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_noticeSó 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.
O telão (viewer), o embed no OBS e as telas de comando NÃO recebem este evento: a agenda é peso que elas não usam, num caminho que não pode atrasar a operação ao vivo. Se a sua integração precisa da agenda, conecte com output=agenda. Uma falha de leitura no servidor não emite nada — a edição já foi gravada e o próximo primeiro quadro traz a agenda nova.

Campos de agenda

CampoDescrição
agenda.speakersCatálogo de palestrantes da sala. Array, nunca nulo.
agenda.speakers[].idIdentificador do palestrante. É o valor usado em sessions[].speaker_ids.
agenda.speakers[].nameNome de exibição. Sempre presente.
agenda.speakers[].title_companyCargo e empresa. Ausente quando não preenchido.
agenda.speakers[].bioMini-biografia. Ausente quando não preenchida.
agenda.speakers[].photo_urlURL https da foto. Ausente quando não preenchida.
agenda.locationsLocais do evento. Array, nunca nulo.
agenda.locations[].idIdentificador do local. É o valor usado em sessions[].location_id.
agenda.locations[].nameNome do local. Sempre presente.
agenda.locations[].pin_xPosição horizontal do pino no mapa, de 0 a 1, relativa à imagem. Ausente quando o local não tem pino.
agenda.locations[].pin_yPosição vertical do pino no mapa, de 0 a 1, relativa à imagem. Ausente quando o local não tem pino.
agenda.sessionsBloco 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_idIdentificador do timer a que a sessão corresponde. É a chave para casar com o array timers do estado da sala.
agenda.sessions[].descriptionDescrição pública da sessão. Ausente quando não preenchida.
agenda.sessions[].trackTrilha da sessão. Ausente quando não preenchida.
agenda.sessions[].location_idIdentificador do local da sessão, entre os de locations. Ausente quando a sessão não tem local.
agenda.sessions[].speaker_idsIdentificadores dos palestrantes da sessão, entre os de speakers. Array, nunca nulo; pode vir vazio.
agenda.event_mapMapa do evento, um por sala. Ausente quando a sala não tem mapa.
agenda.event_map.image_urlURL 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": []
  }
}
Os três arrays são sempre arrays: nunca vêm nulos e nunca vêm ausentes. Um cliente que itera speakers, locations e sessions sem conferir não quebra. Só event_map pode estar ausente.

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.

Só existe com a comunicação régie-palco ligada
Tudo o que está nesta seção depende, em cada sala, de settings.stage_signals.enabled (sinais) e settings.stage_signals.ack_enabled (visto), ambos desligados por padrão. Com os dois desligados, ack_message e send_signal são ignorados em silêncio e nada novo é emitido: nem message_ack, signal e signal_update, nem os campos message_acks e signals no estado da sala. Use room.capabilities.stage_signals para saber se o recurso está disponível.

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' }));
}
O servidor envia um ping a cada 30 segundos. Responda com pong para manter a conexão ativa.