Pular para o conteúdo

Webhooks

Os webhooks enviam uma solicitação HTTP POST ao seu sistema quando ocorre um evento, como a entrada, a chamada ou a conclusão de uma senha.

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

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 em uma fila
ticket.calledUm membro da equipe chama uma senha ao balcão de atendimento
ticket.completedUma senha é marcada como concluída
ticket.cancelledUm cliente ou membro da equipe cancela uma senha
ticket.skippedUma senha é pulada (um adiamento chamável de novo)
ticket.no_showUma senha chamada ou pulada é 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 padrão é um envelope JSON. A forma do campo payload varia de acordo com 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 padrão 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 (assinado 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 em um 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 salvar, no servidor — o feedback do editor espelha exatamente as regras do servidor.

O Cloudflare Queues faz a tentativa inicial e repete até 3 vezes quando recebe uma resposta diferente de 2xx ou ocorre um erro de conexão. O intervalo entre as tentativas aumenta a cada falha. Se todas falharem, a entrega vai para a fila de mensagens mortas e aumenta o contador de falhas consecutivas do endpoint.

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.

A aba Entregas de cada endpoint mostra os últimos 30 dias de tentativas de entrega: tipo de evento, estado HTTP, número de tentativas e registro de data/hora. Use o botão Carregar mais para percorrer registros 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 salvar, 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 receptor imediatamente após a rotação.

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

Quando rodar:

  • Suspeita de comprometimento: o segredo apareceu em um arquivo de registro, foi compartilhado com um funcionário que saiu, ou foi capturado em uma gravação de tela.
  • Higiene de rotina: rodar periodicamente limita o impacto de uma exposição não detectada.
  • Depois de qualquer alteração a que serviços podem ler o segredo (rotação de gestão de chaves).

Como atualizar o receptor:

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

Se perdeu o novo segredo antes de o salvar:

Execute novamente. Cada rotação gera um novo segredo aleatório. O texto simples anterior não pode ser recuperado — apenas uma forma criptografada com AEAD é salva no servidor.