Skip to content

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 ​

EventoQuando
customer.created / customer.updatedCliente cadastrado / alterado
vehicle.created / vehicle.updatedVeículo cadastrado / alterado
service_order.createdOS aberta (check-in)
service_order.updatedOS alterada (relato, diagnóstico, itens do orçamento)
service_order.stage_changedOS mudou de etapa no quadro
service_order.approvedUm item do orçamento foi aprovado (pela equipe ou pelo cliente no link)
service_order.approval_requestedLink de aprovação gerado/enviado ao cliente
customer.reminder_sentLembrete de retorno enviado a um cliente
service_order.readyOS chegou a uma etapa de "pronto para retirada" — bom gatilho para avisar o cliente
service_order.deliveredCarro entregue
job.assignedServiço atribuído (ou tirado) de um técnico
job.started / job.paused / job.blocked / job.finishedTé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']);
}