Tema
Webhooks
Webhooks avisam o seu sistema na hora em que algo acontece.
Cadastrar
No painel, Webhooks → Novo endpoint: informe uma URL https:// pública e escolha os eventos. Guarde o segredo (whsec_...) mostrado — ele serve para verificar a assinatura.
Eventos
| Evento | Quando |
|---|---|
customer.created / customer.updated | Cliente cadastrado / alterado |
vehicle.created / vehicle.updated | Veículo cadastrado / alterado |
service_order.created | OS aberta (check-in) |
service_order.updated | OS alterada (relato, diagnóstico, itens do orçamento) |
service_order.stage_changed | OS mudou de etapa no quadro |
service_order.approved | Um item do orçamento foi aprovado (pela equipe ou pelo cliente no link) |
service_order.approval_requested | Link de aprovação gerado/enviado ao cliente |
customer.reminder_sent | Lembrete de retorno enviado a um cliente |
service_order.ready | OS chegou a uma etapa de "pronto para retirada" — bom gatilho para avisar o cliente |
service_order.delivered | Carro entregue |
job.assigned | Serviço atribuído (ou tirado) de um técnico |
job.started / job.paused / job.blocked / job.finished | Técnico iniciou, pausou, travou (com motivo) ou concluiu um serviço |
data traz a OS completa nos eventos service_order.* e o serviço (com placa e técnico) nos eventos job.*.
Formato
http
POST /seu-endpoint
Content-Type: application/json
X-Proton-Event: service_order.ready
X-Proton-Signature: t=1730000000000,v1=5f2b...
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a3ce929d0e0e4736-01
{ "id": "evt_...", "type": "service_order.ready", "created_at": 1730000000000, "tenant_id": 1, "data": { "id": 42, "number": 17, "stage_kind": "ready", "vehicle": { "plate": "ABC1D23" }, ... } }Responda 2xx em até 10 s. Se falhar, tentamos de novo em 1 min, 5 min, 30 min, 2 h e 12 h. Use o id do evento para não processar duas vezes. Registre o traceparent nos seus logs: ele liga o evento à operação que o gerou.
Verificar a assinatura
Calcule HMAC-SHA256(segredo, "<t>.<corpo bruto>") em hexadecimal e compare com v1. Rejeite se t tiver mais de 5 minutos.
js
import crypto from 'node:crypto'
export function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() - Number(t)) > 5 * 60_000) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}python
import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() * 1000 - int(parts["t"])) > 5 * 60_000:
return False
msg = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])apex
public static Boolean verify(String secret, String header, String rawBody) {
Map<String, String> p = new Map<String, String>();
for (String kv : header.split(',')) { p.put(kv.substringBefore('='), kv.substringAfter('=')); }
Long t = Long.valueOf(p.get('t'));
if (Math.abs(System.currentTimeMillis() - t) > 5 * 60 * 1000) return false;
Blob mac = Crypto.generateMac('hmacSHA256', Blob.valueOf(p.get('t') + '.' + rawBody), Blob.valueOf(secret));
return EncodingUtil.convertToHex(mac) == p.get('v1');
}php
function verify(string $secret, string $header, string $rawBody): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(round(microtime(true) * 1000) - (int)$p['t']) > 300000) return false;
$expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $p['v1']);
}