Webhooks
Em vez de perguntares ao Marcai de tempos a tempos, indica um endereço https:// teu no painel (Integrações → Programadores) e escolhe os eventos. O Marcai faz um POST a cada evento. Cada endereço tem o seu segredo, mostrado uma só vez; podes testar, pausar, gerar um segredo novo e reenviar entregas.
| Evento | Quando |
|---|
| booking.created | Marcação criada, por qualquer canal. |
| booking.updated | Confirmada, remarcada, movida na agenda, check-in, atendimento iniciado, falta. |
| booking.cancelled | Cancelada (pelo cliente ou pelo negócio), recusada ou expirada. |
| booking.completed | Marcação concluída. |
| payment.succeeded | Pagamento recebido. |
| client.created | Cliente novo. |
O pedido
O corpo é sempre { id, type, created_at, data }. Os ids de data são os mesmos da API.
| Cabeçalho | Valor |
|---|
| Marcai-Signature | t=<segundos unix>,v1=<assinatura em hexadecimal> |
| Marcai-Event | O tipo do evento, por exemplo booking.created. |
| Marcai-Event-Id | Igual ao campo id do corpo. Usa-o para ignorar repetições. |
| Marcai-Attempt | Número da tentativa (1 a 10). |
{
"id": "evt_01hx…",
"type": "booking.created",
"created_at": "2026-10-11T10:00:00.000Z",
"data": {
"id": "…",
"status": "confirmed",
"previous_status": null,
"start_at": "2026-10-12T09:00:00.000Z",
"end_at": "2026-10-12T09:30:00.000Z",
"price": 1500,
"service_id": "…",
"staff_id": "…",
"client_id": "…",
"source": "PUBLIC_SITE",
"rescheduled_from_id": null,
"change": "created"
}
}
Verificar a assinatura
Antes de confiares num aviso, confirma que foi o Marcai que o enviou: v1 = HMAC-SHA256(segredo, "<t>.<corpo em bruto>"), em hexadecimal. Usa o corpo em bruto, antes de qualquer conversão para JSON, compara com tempo constante e recusa avisos com t a mais de 5 minutos de diferença do teu relógio (repetições maliciosas).
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const SECRET = process.env.MARCAI_WEBHOOK_SECRET; // whsec_…, mostrado uma só vez no painel
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const received = parts.v1 ?? '';
return (
expected.length === received.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(received))
);
}
const app = express();
// Usa o corpo em bruto: se o converteres para JSON antes, a assinatura deixa de bater.
app.post('/marcai', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verify(rawBody, req.header('Marcai-Signature') ?? '', SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(rawBody);
// Ignora o que já processaste (event.id) e responde logo; trata o resto depois.
res.sendStatus(200);
});
app.listen(3000);
Depois de gerares um segredo novo, o anterior deixa de assinar: troca-o no teu servidor logo a seguir.
Entrega e novas tentativas
- Entrega com sucesso = resposta 2xx em menos de 5 segundos. Responde depressa e processa depois.
- Se falhar, o Marcai volta a tentar até 10 vezes em cerca de 24 horas (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h e 10 h).
- O mesmo evento pode chegar mais do que uma vez (tentativas, reenvio manual): ignora os ids que já viste.
- A ordem não é garantida entre eventos diferentes: usa created_at.
- Se 5 entregas seguidas esgotarem as tentativas, o endereço é desativado e o dono do negócio recebe um email. Reativa-o no painel e reenvia o que falhou.
- O endereço tem de ser https:// e público. Redirecionamentos não são seguidos.
- Os avisos só levam ids, estados e valores — nunca nomes nem contactos. Para o resto, lê a API com o id recebido.