Webhooks: comunicar eventos aos teus próprios sistemas

Depois deste artigo, tens um webhook configurado que chama a tua própria morada nos eventos que escolheres — com chave de assinatura, entrega de teste, registo e repetição automática em caso de erro.

Um webhook inverte o sentido: não é o teu sistema que pergunta ao Univents, é o Univents que contacta o teu sistema assim que algo acontece — chega um pedido, um orçamento é aceite ou é emitida uma fatura. Assim, programas, scripts ou serviços de automatização próprios ligam-se ao teu workspace, sem que ninguém tenha de consultar regularmente.

A configuração faz-se no próprio workspace, em Integrações → Programadores → Webhooks. A configuração, a chave e o registo só são visíveis para o Proprietário — afinal, é lá que constam as moradas que o Univents chama.

Quando compensa usar um webhook

Usa um webhook quando o teu próprio sistema deve reagir de imediato: espelhar um pedido no CRM próprio, acionar o envio para um sistema de armazém, registar uma fatura na tua contabilidade, atualizar um painel de informação. Para tudo o que deve ficar dentro do Univents, o local certo são as Automatizações.

Criar endpoint

  1. Clica no menu principal à esquerda em Integrações, depois à esquerda na categoria Programadores, e abre o cartão Webhooks.
  2. Clica em Criar webhook, em cima à direita.
  3. Em Nome, escreve algo que reconheças mais tarde, por exemplo CRM-Sync.
  4. Em Morada, introduz a morada https:// completa do teu destinatário. Apenas https — as moradas não encriptadas são rejeitadas. O Univents também recusa moradas na tua rede local, em localhost ou numa rede interna.
  5. Assinala os Eventos que este destinatário deve receber.
  6. Guarda. O Univents mostra-te agora a chave de assinatura uma única vez.

A chave de assinatura

A chave começa por whsec_. É apresentada exatamente uma vez — ao criar e ao gerar de novo. Depois, vês apenas o início, para que o workspace permita perceber que chave está registada sem que o valor em si fique à vista. Copia-a, por isso, de imediato para o teu sistema (variável de ambiente, secret store).

Com Gerar nova chave de assinatura, obténs uma nova a qualquer momento. A anterior fica logo inválida — substitui-a do teu lado no mesmo instante, caso contrário o teu destinatário rejeita as mensagens recebidas.

Verificar a assinatura

O Univents envia três cabeçalhos em cada entrega:

CabeçalhoConteúdo
X-Univents-Signaturet=<Zeitstempel>,v1=<Signatur>
X-Univents-Eventa chave do acionador, por exemplo inquiry_created
X-Univents-Delivery-Ido identificador desta entrega

A assinatura é um HMAC-SHA256 sobre o texto ${t}.${Body} — ou seja, carimbo de data/hora, um ponto e o corpo em bruto, byte a byte, tal como chega. Importante: verifica o corpo em bruto, não um corpo serializado de novo. Um JSON.parse seguido de JSON.stringify altera a ordem dos campos e os escapes, e a verificação falha, embora a mensagem esteja correta.

Eis como fica a verificação em Node:

const crypto = require('node:crypto');
const express = require('express');

const app = express();

// Roher Body — NICHT express.json() davor.
app.post('/webhooks/dein-system', express.raw({ type: '*/*' }), (req, res) => {
  const header = req.get('X-Univents-Signature') ?? '';
  const parts = Object.fromEntries(
    header.split(',').map((piece) => piece.trim().split('=')),
  );

  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body.toString('utf8')}`)
    .digest('hex');

  const received = parts.v1 ?? '';

  // Länge zuerst prüfen: timingSafeEqual wirft bei ungleicher Länge.
  const ok =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!ok) return res.status(400).send('signature mismatch');

  // Der Zeitstempel liegt unter der Signatur — so kann niemand eine alte
  // Meldung erneut einspielen.
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (ageSeconds > 300) return res.status(400).send('stale');

  const event = JSON.parse(req.body.toString('utf8'));
  console.log(req.get('X-Univents-Event'), event);

  res.sendStatus(200);
});

Responde com 200 (ou outra resposta no intervalo 2xx) assim que tiveres aceitado a mensagem. Qualquer outra resposta, ou uma resposta que demore mais de dez segundos, conta como tentativa falhada.

Entregas: o que acontece em caso de erro

A entrega é feita pelo menos uma vez — não exatamente uma vez. Uma falha de ligação pelo caminho pode fazer com que a mesma operação chegue uma segunda vez. Constrói, por isso, o teu destinatário de modo a que uma chamada repetida não estrague nada: antes de criares seja o que for, compara o X-Univents-Delivery-Id com os identificadores processados mais recentemente.

Se o teu destinatário não responder ou responder com um erro, o Univents tenta de novo. Estão previstas oito tentativas no total, com intervalos crescentes:

TentativaIntervalo desde a anterior
1imediatamente
21 minuto
35 minutos
430 minutos
52 horas
66 horas
712 horas
824 horas

Depois disso, a mensagem é considerada definitivamente falhada e não volta a ser tentada. Se o teu sistema tiver manutenção programada, tens assim quase dois dias para voltar a estar acessível.

Entrega de teste

Com Teste, envias uma mensagem a um endpoint sem esperar por um evento real. Assim, depois de configurares, verificas se a morada, a verificação da assinatura e o caminho de resposta estão corretos. A entrega de teste fica depois no registo, para não a confundires com uma mensagem real.

Um endpoint em pausa não recebe entregas de teste — ativa-o primeiro. E, como a entrega de teste é a única forma de levares o nosso servidor a chamar uma morada à tua escolha, aplica-se um limite de dez entregas de teste por workspace e por hora. Depois disso, a interface indica que deves tentar mais tarde.

Registo e nova entrega

Sob cada endpoint constam as últimas 50 entregas. Em cada linha vês:

  • o estado — em espera, em curso, entregue ou falhou definitivamente,
  • o evento,
  • o número de tentativas (por exemplo 3/8),
  • o estado da resposta do teu destinatário.

Com Mostrar conteúdo, vês a mensagem enviada; com Detalhes, o último erro e o momento da próxima tentativa. O Univents guarda a resposta do teu destinatário de forma abreviada, como material de diagnóstico.

Entregar novamente envia mais uma vez uma mensagem entregue ou falhada definitivamente — útil quando o teu sistema esteve indisponível apenas por pouco tempo. Enquanto uma entrega estiver em curso ou ainda em espera, o botão fica bloqueado, para que não surjam duas chamadas em simultâneo.

Pausar e eliminar

O interruptor no endpoint coloca-o em pausa: o Univents deixa de entregar de imediato, mas a configuração mantém-se — ideal para um sistema em reformulação. O Univents lê o estado de novo em cada entrega, pelo que a pausa não só entra em vigor no evento seguinte.

Eliminar remove o endpoint, incluindo o respetivo registo. O Univents pede confirmação antes.

Eventos disponíveis

Podes escolher os mesmos eventos que as Automatizações conhecem. A lista no diálogo vai crescendo: assim que o Univents passa a comunicar um evento num novo ponto, ele fica também disponível para webhooks. Um evento sem seleção não é entregue.

O que consta na mensagem

Todas as mensagens têm o mesmo envelope:

{
  "id": "48213",
  "type": "inquiry_created",
  "created_at": "2026-09-28T10:12:33.000Z",
  "data": { "…": "…" }
}
  • id — o identificador desta entrega, idêntico ao cabeçalho X-Univents-Delivery-Id. Usa-o como chave de idempotência.
  • type — a chave do acionador (inquiry_created, event_confirmed, …).
  • created_at — quando o envelope foi construído, não quando o evento ocorreu.
  • data — o conteúdo funcional, segundo uma lista fixa para cada evento.

Em data consta apenas o que está nessa lista. Campos internos e tudo o que possa conter credenciais ou texto livre são omitidos — entre outros legacy_*, stripe_* (identificadores de pagamento), *token*, *secret*, *_hash*, pdf_url, campos de e-mail e telefone, notas/descrições, bem como interruptores internos (is_*, created_by, deleted_at). Um campo sem valor é omitido, não enviado como null. Novas colunas nas nossas tabelas não aparecem aqui por si; incluímo-las deliberadamente, uma a uma.

Cada versão de data inclui entity_type e entity_id — assim, associas a mensagem mesmo quando o evento não tem um registo próprio.

Pedidos

EventoCampos em data
inquiry_createdid, event_id, contact_id, location_id, booking_page_id, booking_request_state, event_name, event_date, event_end_date, guest_count, estimated_value, account_id, created_at, updated_at
quote_acceptedos campos de inquiry_created e os de um orçamento (ver Documentos financeiros)

Eventos

EventoCampos em data
event_confirmed, event_cancelledid, name, guest_count, budget, start_date, end_date, date_is_confirmed, language, event_state, location_id, room_id, contact_id, account_id, created_at, updated_at
event_t_minuscomo acima, mais days_before, weekday
event_t_pluscomo acima, mais days_after

Documentos financeiros

Aplica-se a invoice_overdue, invoice_paid, quote_sent, quote_expired e quote_rejected; em quote_rejected, adicionalmente source.

id, event_id, contact_id, doc_type, document_number, title, version, quote_state, invoice_state, issue_date, due_date, sent_date, accepted_date, rejected_date, expires_at, net_total, gross_total, tax_total, discount_total, deposit_amount, currency, linked_order_id, prior_version_id, account_id, created_at, updated_at

Outros eventos

EventoCampos em data
payment_receivedid, booking_request_id, event_id, contact_id, booking_order_state, total_net, total_gross, total_tax, discount_total, currency, payment_method, paid_at, account_id, created_at, updated_at
contract_signedid, event_id, contact_id, title, contract_doc_type, contract_state, version, sent_at, expires_at, has_signed_pdf, account_id, created_at, updated_at
email_receivedcontact_id, received_at, has_attachments, has_contact_match — sem conteúdo do e-mail, sem assunto
schedule_monthlyday_of_month, target_date, period_start, period_end, period_label, account_id

A entrega de teste usa o mesmo envelope, com type: "webhook_test" e data: { test, endpoint_id, sent_at }.

Um evento para o qual não consta aqui nenhuma lista chega com data vazio: mais vale um envelope vazio e visível do que um registo em bruto.

Limites

  • Apenas https, sem redirecionamentos: se o teu servidor responder com um redirecionamento, conta como tentativa falhada. Introduz a morada final.
  • Dez segundos por tentativa. Trabalho que exija mais tempo deve ficar para depois da resposta: aceita a mensagem, responde com 200 e processa-a em seguida.
  • O Conteúdo da mensagem descreve o registo que desencadeou o evento — trata-o como dados de clientes do teu workspace e não o registes em log sem verificação.

Newsletter Univents

Novidades do produto e dicas práticas para empresas de eventos e catering. Cerca de uma vez por mês, não mais.

A newsletter é enviada em inglês.