
O webhook é a forma de a sua loja, o seu sistema de faturamento ou o seu bot saber que o cliente pagou. A SnapEX Pay envia um HTTP POST para a sua URL a cada evento de fatura, pagamento e saque. Bem implementado, ele é o sinal mais rápido e confiável que você tem. Feito com descuido, vira um jeito de marcar como pagos pedidos que nunca foram pagos. Este guia mostra o que importa.
Configure o webhook
No painel do comerciante, abra a página “Desenvolvedores” → bloco “Webhook”, informe a sua URL e clique em “Salvar”. Cada conta tem uma única URL, que recebe todos os tipos de evento. Ao salvar pela primeira vez, o painel mostra uma única vez a chave secreta whsec_…, usada na assinatura HMAC: guarde-a apenas no seu servidor.
A URL precisa ser um endereço https:// absoluto, com host público: sem login:password, sem localhost, e o host não pode resolver para um IP privado ou reservado. Redirecionamentos não são seguidos, então informe a URL final.
O que você recebe
Toda requisição traz quatro headers:
| Header | Significado |
|---|---|
Webhook-Id |
ID único do evento. Não muda nas novas tentativas nem nos reenvios manuais. |
Webhook-Timestamp |
Tempo Unix (em segundos) em que esta tentativa foi enviada. Cada nova tentativa tem um valor novo. |
Webhook-Signature |
v1= e o HMAC-SHA256 em hex minúsculo de {Webhook-Timestamp}.{raw body}. |
Content-Type |
Sempre application/json. |
O corpo tem id (igual ao Webhook-Id), type, created_at e data. Nos eventos invoice.*, data é a fatura no mesmo formato de GET /invoices/{id}, incluindo environment, amount_usd, status e o seu external_id.
Os eventos de fatura:
| Evento | Quando |
|---|---|
invoice.created |
Fatura criada, ainda sem forma de pagamento escolhida. |
invoice.awaiting |
A forma de pagamento foi escolhida e o endereço foi gerado. |
invoice.underpaid |
Chegou parte do valor; o mesmo endereço aguarda o restante. |
invoice.paid |
O valor total foi pago e creditado. O único sinal para liberar o pedido. |
invoice.partially_paid |
Só parte do valor foi paga, o prazo para completar o pagamento terminou e a parte recebida foi creditada a você. |
invoice.expired |
O prazo da fatura terminou sem pagamento. |
invoice.extra_payment |
Chegou outra transferência depois que a fatura já estava paga; ela é creditada à parte. |
Verifique a assinatura HMAC
- Leia o corpo bruto da requisição como bytes, antes de qualquer parsing de JSON. Um JSON serializado de novo não vai bater.
- Calcule o HMAC-SHA256 de
{Webhook-Timestamp}.{raw body}usando como chave a chave secreta inteira, incluindo o prefixowhsec_, e codifique o resultado em hex. - Compare
v1=+ hex com o headerWebhook-Signatureusando uma função de comparação em tempo constante. - Rejeite o
Webhook-Timestampque diferir do seu relógio em mais de 5 minutos. As novas tentativas trazem um timestamp novo, então essa regra não as atrapalha.
PHP:
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… from the cabinet
$body = file_get_contents('php://input'); // raw body, before json_decode
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// skip if $event['id'] was already processed; do the heavy work in a queue
http_response_code(200);
Node.js (Express):
const crypto = require('crypto');
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('Webhook-Timestamp') || '';
const sig = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto
.createHmac('sha256', process.env.SNAPEX_WEBHOOK_SECRET)
.update(ts + '.' + req.body)
.digest('hex'));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
if (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(sig, expected)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// skip if event.id was already processed; queue the heavy work
res.sendStatus(200);
});
Novas tentativas, entregas duplicadas e ordem
- Sucesso é qualquer 2xx. Qualquer outro código, timeout ou erro de rede conta como falha. Os timeouts são de 5 segundos para conectar e de 15 segundos para a requisição inteira, então responda rápido e faça o trabalho pesado de forma assíncrona.
- Novas tentativas. Até 5 tentativas por evento: a primeira e, depois de cada falha, novas tentativas após cerca de 10 segundos, 1 minuto, 5 minutos e 15 minutos. Depois da quinta falha, a entrega automática para; o painel mostra as entregas recentes no bloco “Entregas do webhook” e tem o botão “Reenviar”, que envia o evento de novo com o mesmo
Webhook-Id. - Entregas duplicadas. A entrega é do tipo at-least-once (pelo menos uma vez): o mesmo evento pode chegar mais de uma vez. Guarde os valores de
Webhook-Idjá processados e ignore as repetições. - Ordem. Os eventos podem chegar fora de ordem: um evento antigo, enviado em nova tentativa, pode chegar depois de um mais recente. Não dependa da ordem; compare o status que vem no evento com o que você já tem.
- Confira de novo, se quiser. Depois de um webhook, você pode consultar o objeto outra vez pela API:
GET /invoices/{id}.
Teste sem dinheiro real
Crie uma fatura de teste ("environment": "test"), pague-a na página de pagamento e acompanhe a chegada dos webhooks com data.environment = "test". Detalhes: Como testar pagamentos em cripto sem dinheiro real.
Os módulos prontos para WHMCS, BILLmanager, WooCommerce e outras plataformas já verificam a assinatura HMAC e nunca creditam um pagamento duas vezes; veja pay.snapex.pro. Referência completa: documentação da API da SnapEX Pay.