Synergy Connect

Documentação

Assinatura dos webhooks

Toda entrega de webhook vem assinada, para você ter certeza de que foi a Synergy quem enviou, e de que o corpo não foi alterado no caminho. Há dois cabeçalhos de assinatura, com o mesmo secret do webhook:

CabeçalhoFormatoProtege contraQuando usar
X-Synergy-Signaturet=<segundos>,v1=<64 hex>Corpo alterado e reenvio de uma entrega capturada (tem carimbo de tempo)Preferido. Use sempre que ele vier
X-Hub-Signature-256sha256=<64 hex>Corpo alteradoCompatível com bibliotecas feitas para a Meta, e entregas antigas

Os dois chegam em toda tentativa, inclusive retentativas. O secret (32 caracteres hexadecimais) aparece só na criação do webhook e em POST /v1/webhooks/{hook_id}/rotate-secret (Webhooks).

Regras que valem para os dois#

  • Calcule a assinatura sobre os bytes exatos do corpo, antes de qualquer JSON.parse. Reserializar o JSON muda os bytes e invalida a assinatura. No Express, use express.raw({ type: 'application/json' }); no Flask, request.get_data().
  • O secret é usado como texto UTF-8, do jeito que está. Segredo vazio é erro de configuração: nunca aceite a entrega nesse caso.
  • Compare em tempo constante (timingSafeEqual, hmac.compare_digest), nunca com ==.
  • Confira o formato antes de comparar. Assinatura inválida: responda 401 e não processe.

X-Synergy-Signature (com carimbo de tempo)#

O cabeçalho tem a forma:

  • t: o momento desta tentativa, em segundos desde 1970. Cada retentativa tem um t novo.
  • v1: o HMAC-SHA256, em hexadecimal minúsculo, do texto <t>.<X-Synergy-Delivery-Id>.<corpo cru>, com o secret como chave. O X-Synergy-Delivery-Id entra na conta: quem troca o id invalida a assinatura.

Para validar:

  1. Leia t (de 1 a 12 dígitos, ^\d{1,12}$) e v1 (64 hexadecimais minúsculos). Qualquer outro formato é inválido.
  2. Recuse se |agora − t| passar de 300 segundos: é isso que impede reenviar uma entrega capturada.
  3. Calcule o HMAC de <t>.<X-Synergy-Delivery-Id>.<corpo cru> e compare com v1 em tempo constante.
  4. Só depois use o X-Synergy-Delivery-Id para descartar entregas repetidas (Webhooks, "Idempotência").

Formato do X-Synergy-Delivery-Id#

O id entra na conta entre dois pontos (<t>.<id>.<corpo>). Por isso o valor do header só tem A-Z, a-z, 0-9, _ e -, de 1 a 200 caracteres (^[A-Za-z0-9_-]{1,200}$). Recuse a entrega antes de calcular o HMAC se o header não casar. O id do evento é normalizado pela Synergy antes de sair, com uma regra que mantém o resultado legível e nunca junta dois ids diferentes:

No id do eventoNo header
___
:_c
._d
qualquer outro caractere fora de A-Z, a-z, 0-9 e -_x mais os dois hexadecimais minúsculos de cada byte UTF-8 (+ é _x2b, espaço é _x20, é é _xc3_xa9)

Por exemplo, ev-abc:1a2b3c4d chega como ev-abc_c1a2b3c4d, e ev-3f9a…:0b9c4d1e+12 chega como ev-3f9a…_c0b9c4d1e_x2b12. Se o resultado passa de 200 caracteres, o id vira os 100 primeiros, _h e os 32 primeiros hexadecimais do SHA-256 do id original.

A regra é determinística: o mesmo evento tem o mesmo id em todas as tentativas. Use o valor do header como ele chegou, tanto na conta do HMAC quanto para descartar repetidos; não precisa desfazer o escape.

Vetor de teste#

Para conferir a sua implementação: com o secret 0123456789abcdef0123456789abcdef, o X-Synergy-Delivery-Id ping-3f9a1c0b-7d2e-4a6f-8b1c-2d3e4f5a6b7c, t igual a 1767225600 e o corpo

o cabeçalho esperado é:

(No teste, ignore a tolerância de 300 s: o t é fixo. O vetor é o TIMESTAMPED_SIGNATURE_EXAMPLE publicado em x-webhook-delivery do openapi.json.)

Texto
X-Synergy-Signature: t=1767225600,v1=9baa189aceecd03d75cb0a92954f222a09db72a2a786d079af44beee79609372
Exemplo

JavaScript

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: os bytes EXATOS do corpo (Buffer); headers: req.headers (nomes em minúsculas)
export function isValidTimestampedSignature(rawBody, headers, secret, toleranceSeconds = 300) {
  if (!secret || !secret.trim()) throw new Error('webhook secret is empty');
  const header = headers['x-synergy-signature'];
  const deliveryId = headers['x-synergy-delivery-id'];
  const match = /^t=(\d{1,12}),v1=([0-9a-f]{64})$/.exec(header ?? '');
  if (!match || !deliveryId) return false;
  const t = Number(match[1]);
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret)
    .update(`${t}.${deliveryId}.`)
    .update(rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}

Python

import hashlib
import hmac
import re
import time


def is_valid_timestamped_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300) -> bool:
    if not secret.strip():
        raise ValueError("webhook secret is empty")
    match = re.fullmatch(r"t=(\d{1,12}),v1=([0-9a-f]{64})", headers.get("X-Synergy-Signature", ""))
    delivery_id = headers.get("X-Synergy-Delivery-Id")
    if not match or not delivery_id:
        return False
    t = int(match.group(1))
    if abs(int(time.time()) - t) > tolerance_seconds:
        return False
    signed = f"{t}.{delivery_id}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, match.group(2))
Texto
{"object":"whatsapp_business_account","entry":[{"id":"0","time":1767225600,"changes":[{"field":"synergy_ping","value":{"messaging_product":"whatsapp","ping":true,"timestamp":"1767225600"}}]}]}
Texto
X-Synergy-Signature: t=1767225600,v1=9baa189aceecd03d75cb0a92954f222a09db72a2a786d079af44beee79609372

X-Hub-Signature-256 (formato da Meta)#

sha256= mais o HMAC-SHA256, em hexadecimal minúsculo, só do corpo, com o secret. Sem carimbo de tempo: quem captura uma entrega pode reenviá-la, e por isso este cabeçalho vale como segunda opção.

Com o mesmo secret e o mesmo corpo acima, o cabeçalho é:

O código de validação (JavaScript e Python) está em Webhooks, "Validar a assinatura".

Texto
X-Hub-Signature-256: sha256=b34c62852bf9eb4132dbac56d4b909a1e62d3df61b21807329b6c1f4d59ec77e

Qual cabeçalho conferir#

Use o X-Synergy-Signature quando ele estiver presente, e caia para o X-Hub-Signature-256 quando não estiver (um receptor de terceiros, uma entrega antiga):

Não aceite "qualquer um dos dois": se o X-Synergy-Signature veio e falhou, a entrega é inválida, mesmo que o outro cabeçalho confira. O SDK (webhooks.constructEvent) e o n8n já fazem exatamente isso.

Exemplo
function verifyDelivery(rawBody, headers, secret) {
  if (headers['x-synergy-signature']) {
    return { ok: isValidTimestampedSignature(rawBody, headers, secret), timestamped: true };
  }
  return { ok: isValidSignature(rawBody, headers['x-hub-signature-256'], secret), timestamped: false };
}

Erros comuns#

SintomaCausaO que fazer
Nenhuma assinatura confereO corpo foi interpretado e reserializado, ou o framework removeu espaçosLeia o corpo cru, antes do JSON
Só X-Synergy-Signature falhaVocê assinou t.corpo, sem o X-Synergy-Delivery-Id, ou usou o id erradoMonte <t>.<delivery-id>.<corpo>
Falha só em retentativas, com t velhoO relógio do seu servidor está adiantado ou atrasadoSincronize (NTP). A tolerância é de 300 s
Tudo falhou depois de trocar o segredoVocê rotacionou com rotate-secret e ainda usa o antigoAtualize o segredo no seu servidor
X-Synergy-Signature ausenteEntrega de um receptor que não passa o cabeçalho adiante, ou entrega antigaCaia para X-Hub-Signature-256

Ficou com dúvida? Escreva para suporte@synergyconnect.com.br.