Pular para o conteúdo

Webhooks

Os webhooks permitem que os seus sistemas recebam notificações HTTP POST em tempo real quando ocorrem eventos de fila — uma senha entra, é chamada, é concluída, e assim por diante.

  1. Abra admin.jonot.io/settings/integrations.
  2. Clique em Adicionar endpoint.
  3. Introduza um URL HTTPS público que o seu servidor esteja a escutar.
  4. Escolha a que eventos quer subscrever.
  5. Clique em Guardar.

Guarde o seu segredo de assinatura — é mostrado apenas uma vez. Para verificar se o seu endpoint está acessível, use o botão Enviar pedido de teste na página de edição do endpoint.

Nome do eventoQuando dispara
ticket.joinedUm cliente entra numa fila
ticket.calledUm membro do pessoal chama uma senha ao balcão de atendimento
ticket.completedUma senha é marcada como concluída
ticket.cancelledUm cliente ou membro do pessoal cancela uma senha
ticket.skippedUma senha é saltada (um adiamento chamável de novo)
ticket.no_showUma senha chamada ou saltada é confirmada como ausência
queue.status_changedO estado de uma fila muda (ACTIVE, PAUSED ou CLOSED)

Toda a entrega é um HTTP POST com:

Content-Type: application/json
X-Jonot-Event: ticket.called
X-Jonot-Delivery-Id: <uuid>
X-Jonot-Timestamp: 2024-06-01T12:00:00.000Z
X-Jonot-Signature: v1,<base64-hmac-sha256>
X-Jonot-Payload-Version: v1

O corpo predefinido é um envelope JSON. A forma do campo payload varia consoante o evento:

ticket.joined

{
"deliveryId": "<uuid>",
"event": "ticket.joined",
"timestamp": "2024-06-01T12:00:00.000Z",
"org": { "id": "org_…", "name": "My Org" },
"payload": {
"ticket": { "id": "tkt_…", "queueId": "q_…" },
"queue": { "id": "q_…", "name": "Main Queue" },
"location": { "id": "loc_…", "name": "Downtown" }
}
}

ticket.called / ticket.completed / ticket.skipped / ticket.no_show

{
"deliveryId": "<uuid>",
"event": "ticket.called",
"timestamp": "2024-06-01T12:00:00.000Z",
"org": { "id": "org_…", "name": "My Org" },
"payload": {
"ticket": {
"id": "tkt_…",
"number": 42,
"status": "CALLED",
"queueId": "q_…",
"createdAt": "2024-06-01T11:58:00.000Z",
"updatedAt": "2024-06-01T12:00:00.000Z"
}
}
}

ticket.cancelled

{
"deliveryId": "<uuid>",
"event": "ticket.cancelled",
"timestamp": "2024-06-01T12:00:00.000Z",
"org": { "id": "org_…", "name": "My Org" },
"payload": {
"ticketId": "tkt_…",
"queueId": "q_…"
}
}

queue.status_changed

{
"deliveryId": "<uuid>",
"event": "queue.status_changed",
"timestamp": "2024-06-01T12:00:00.000Z",
"org": { "id": "org_…", "name": "My Org" },
"payload": {
"queueId": "q_…",
"status": "PAUSED"
}
}

O Jonot assina cada entrega com HMAC-SHA256 sobre a cadeia:

<deliveryId>.<timestamp>.<body>

Para verificar em Node.js (≥18):

import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Returns true when the signature header is valid and the timestamp is
* within 5 minutes of now. Throws for malformed input.
*/
function verifySignature(secret, deliveryId, timestamp, body, header) {
// Replay-attack guard: reject deliveries older than 5 minutes.
const ageMs = Date.now() - new Date(timestamp).getTime();
if (Math.abs(ageMs) > 5 * 60 * 1000) return false;
const expected =
"v1," +
createHmac("sha256", secret)
.update(`${deliveryId}.${timestamp}.${body}`)
.digest("base64");
// timingSafeEqual prevents timing-oracle attacks.
// Buffers must be the same length — if lengths differ the signature is
// invalid, but we still compare a dummy value to keep constant time.
const expectedBuf = Buffer.from(expected);
const headerBuf = Buffer.from(header);
if (expectedBuf.length !== headerBuf.length) return false;
return timingSafeEqual(expectedBuf, headerBuf);
}

Pode substituir o corpo JSON predefinido por um modelo personalizado. Os modelos usam uma sintaxe Mustache-lite:

  • {{ path.to.value }} — substituído e escapado de acordo com o tipo de conteúdo: escapado como cadeia JSON para application/json, codificado em percentagem para application/x-www-form-urlencoded, em bruto para text/plain
  • {{{ path.to.value }}} — substituído em bruto (sem escape)

Exemplo de modelo para application/json (subscrito a ticket.joined):

{
"type": "{{ event }}",
"ticketId": "{{ payload.ticket.id }}",
"queueName": "{{ payload.queue.name }}"
}

O campo de modelo de payload é um editor de código completo com:

  • Realce de sintaxe e correspondência de parênteses para modelos JSON.
  • Paleta de variáveis — uma linha sempre visível de botões de inserção, um por cada caminho de variável disponível para os eventos selecionados. Clique num botão para inserir um token {{ path }} na posição do cursor.
  • Validação em tempo real — à medida que escreve, o editor verifica tanto a estrutura JSON (quando o tipo de conteúdo é application/json e não existem etiquetas {{{ }}} em bruto) como os caminhos de variável. Os diagnósticos aparecem como marcadores incorporados no editor e como uma faixa de resumo por baixo:
    • Erro — um caminho desconhecido em todos os eventos selecionados.
    • Aviso — um caminho que existe apenas para alguns dos eventos selecionados (ficará vazio nas entregas dos outros eventos).

A validação também é executada no momento de guardar, no servidor — o feedback do editor espelha exatamente as regras do servidor.

As entregas falhadas (não-2xx ou erro de ligação) são repetidas até 3 vezes com espera exponencial pelo Cloudflare Queues. Depois de 3 falhas, a entrega vai para a fila de mensagens mortas e o contador de falhas consecutivas do endpoint incrementa.

Assim que um endpoint acumula 20 falhas consecutivas, é desativado automaticamente. Reative-o a partir da página de edição do endpoint; o contador repõe-se a zero.

O separador Entregas de cada endpoint mostra os últimos 30 dias de tentativas de entrega: tipo de evento, estado HTTP, número de tentativas e registo de data/hora. Use o botão Carregar mais para percorrer registos mais antigos.

LimiteValor
Endpoints por organização5
Cabeçalhos personalizados por endpoint10
Comprimento do valor do cabeçalho1024 bytes
Tamanho do modelo de payload16 KB
Tempo limite de entrega10 s
Retenção do histórico de entregas30 dias
Taxa máxima de entrega por organização120 / 60 s
  1. Abra a página de edição do endpoint.
  2. Clique em Rodar segredo de assinatura.
  3. Confirme a rotação na caixa de diálogo.
  4. Copie o novo segredo imediatamente — é mostrado apenas uma vez e não pode ser recuperado. Se fechar a caixa de diálogo sem o guardar, terá de rodar novamente para obter um novo valor em texto simples.
  5. Atualize o seu servidor para verificar as assinaturas com o novo segredo.

Não há nenhuma janela de sobreposição. O segredo antigo deixa de verificar assim que confirmar a rotação. Planeie implementar o novo segredo no seu recetor imediatamente após a rotação.

A interface mostra Segredo ativo: ····XXXX (os últimos quatro carateres) junto ao botão Rodar, para que possa verificar qual o segredo atualmente em vigor — útil para confirmar que o seu recetor e o servidor estão sincronizados após uma rotação.

Quando rodar:

  • Suspeita de comprometimento: o segredo apareceu num ficheiro de registo, foi partilhado com um funcionário que saiu, ou foi capturado numa gravação de ecrã.
  • Higiene de rotina: rodar periodicamente limita o impacto de uma exposição não detetada.
  • Depois de qualquer alteração a que serviços podem ler o segredo (rotação de gestão de chaves).

Como atualizar o recetor:

  1. Rode na interface de admin e copie o novo segredo.
  2. Atualize o segredo no armazenamento de segredos do seu recetor (variável de ambiente, gestor de segredos, etc.).
  3. Implemente o recetor atualizado.
  4. Confirme que a entrega seguinte é bem-sucedida no separador Entregas.

Se perdeu o novo segredo antes de o guardar:

Rode de novo. Cada rotação gera um novo segredo aleatório. O texto simples anterior não é recuperável — apenas é guardado no servidor uma forma encriptada com AEAD.