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
- Clica no menu principal à esquerda em Integrações, depois à esquerda na categoria Programadores, e abre o cartão Webhooks.
- Clica em Criar webhook, em cima à direita.
- Em Nome, escreve algo que reconheças mais tarde, por exemplo
CRM-Sync. - Em Morada, introduz a morada
https://completa do teu destinatário. Apenashttps— as moradas não encriptadas são rejeitadas. O Univents também recusa moradas na tua rede local, emlocalhostou numa rede interna. - Assinala os Eventos que este destinatário deve receber.
- 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çalho | Conteúdo |
|---|---|
X-Univents-Signature | t=<Zeitstempel>,v1=<Signatur> |
X-Univents-Event | a chave do acionador, por exemplo inquiry_created |
X-Univents-Delivery-Id | o 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:
| Tentativa | Intervalo desde a anterior |
|---|---|
| 1 | imediatamente |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 6 horas |
| 7 | 12 horas |
| 8 | 24 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çalhoX-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
| Evento | Campos em data |
|---|---|
inquiry_created | id, 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_accepted | os campos de inquiry_created e os de um orçamento (ver Documentos financeiros) |
Eventos
| Evento | Campos em data |
|---|---|
event_confirmed, event_cancelled | id, 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_minus | como acima, mais days_before, weekday |
event_t_plus | como 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
| Evento | Campos em data |
|---|---|
payment_received | id, 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_signed | id, 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_received | contact_id, received_at, has_attachments, has_contact_match — sem conteúdo do e-mail, sem assunto |
schedule_monthly | day_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.