Verificar a assinatura (HMAC)
A cada webhook entregue, a Selectwin assina o corpo da requisição com HMAC-SHA256 e envia o resultado no cabeçalho X-Selectwin-Signature. Recalcular essa assinatura no seu servidor — e só processar o
A cada webhook entregue, a Selectwin assina o corpo da requisição com HMAC-SHA256 e envia o resultado no cabeçalho X-Selectwin-Signature. Recalcular essa assinatura no seu servidor — e só processar o evento se ela bater — é como você garante que o payload veio mesmo da Selectwin e não foi alterado no caminho. Este guia explica como a assinatura funciona e mostra o passo a passo de verificação em Node.js, Python e PHP.
Atenção
Por que isso importa: qualquer pessoa que descubra a URL do seu endpoint pode enviar requisições forjadas. Antes de confiar em qualquer payload de webhook, verifique a assinatura criptográfica para garantir que ele foi realmente enviado pela Selectwin e não foi adulterado em trânsito. Esta é uma etapa obrigatória para integrações em produção que movimentam dinheiro.
O que é assinar com HMAC
Um webhook é uma notificação HTTP que a Selectwin envia ao seu servidor quando algo acontece (ex.: uma transação foi aprovada). Como qualquer um pode fazer um POST para a sua URL, precisamos de uma prova de origem.
Essa prova é uma assinatura HMAC (Hash-based Message Authentication Code): um resumo criptográfico calculado a partir de duas coisas — o conteúdo da mensagem e um segredo compartilhado que só você e a Selectwin conhecem. Quem não tem o segredo não consegue produzir uma assinatura válida para um corpo qualquer. Ao recalcular a assinatura com o mesmo segredo e comparar, você confirma de uma só vez que (1) o remetente conhece o segredo e (2) o corpo não foi modificado.
Como funciona
A cada entrega, a Selectwin calcula uma assinatura HMAC-SHA256 sobre o corpo bruto (raw body) do evento usando o segredo do seu endpoint e envia o resultado no cabeçalho:
X-Selectwin-Signature: sha256=<hex>- O valor após
sha256=é o HMAC-SHA256 em hexadecimal minúsculo. - O segredo é o
whsec_...retornado uma única vez na criação do endpoint, no camposecretda resposta. Em sandbox/produção o prefixo é o mesmo (whsec_); guarde-o com segurança (variável de ambiente ou cofre de segredos). - A mensagem assinada é exatamente o corpo JSON recebido, byte a byte. Para reproduzir a assinatura você precisa do corpo bruto — não re-serialize o JSON (reordenar chaves, mudar espaçamento ou números de ponto flutuante quebra a verificação).
O corpo que você recebe
O payload entregue é o envelope do evento — a mesma forma documentada no Catálogo de Eventos de Webhook:
{
"id": "wbh_01hqzvabc",
"type": "transaction.approved",
"resource": "transaction",
"resourceId": "tra_987654321",
"createdAt": "2026-04-12T17:56:33.000Z",
"data": { "id": "tra_987654321", "amount": 1500, "status": "approved" }
}| Campo | Descrição |
|---|---|
id | ID do evento (wbh_...). Use-o para deduplicar reentregas. |
type | Tipo do evento (ex.: transaction.approved). Lista completa no Catálogo de Eventos. |
resource / resourceId | Recurso afetado e seu ID público. |
createdAt | Quando o evento foi gerado (ISO 8601 UTC). |
data | O objeto do recurso no momento do evento. Valores monetários (ex.: amount) vêm em centavos — 1500 = R$ 15,00. |
A assinatura é calculada sobre todo esse corpo bruto, não apenas sobre um campo. Verifique antes de fazer
JSON.parse.
Passos da verificação
- Leia o corpo bruto da requisição (os bytes/string exatos, antes de qualquer parsing JSON).
- Extraia a assinatura recebida: o trecho após
sha256=no cabeçalhoX-Selectwin-Signature. - Calcule
HMAC-SHA256(rawBody, endpointSecret)e represente em hexadecimal minúsculo. - Compare o valor recebido com o calculado usando uma comparação de tempo constante (constant-time), que não vaza informação pelo tempo de resposta e protege contra ataques de timing.
- Se baterem, responda
2xxe processe o evento. Se não baterem (ou o cabeçalho estiver ausente), responda401e descarte o evento — não o processe.
Use sempre o corpo bruto
Frameworks que fazem JSON.parse automático e te entregam um objeto já alteraram a representação (espaçamento, ordem das chaves). Re-serializar esse objeto produz bytes diferentes e a assinatura nunca vai bater. Configure o parser para preservar o corpo bruto (Buffer/bytes) — veja os exemplos abaixo.
Exemplos
Os exemplos abaixo leem o segredo de uma variável de ambiente, recalculam o HMAC sobre o corpo bruto e comparam em tempo constante. Substitua a rota e a variável conforme o seu projeto.
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = process.env.SELECTWIN_WEBHOOK_SECRET; // whsec_...
// IMPORTANTE: capture o corpo BRUTO (Buffer). Não use express.json() nesta rota.
app.post('/webhooks/selectwin', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Selectwin-Signature') || '';
const received = header.startsWith('sha256=') ? header.slice(7) : header;
const expected = crypto
.createHmac('sha256', SECRET)
.update(req.body) // req.body é um Buffer (corpo bruto)
.digest('hex');
// Comparação de tempo constante (rejeita também tamanhos diferentes)
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// ... enfileire e processe event.type / event.data ...
res.status(200).send('ok');
});Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["SELECTWIN_WEBHOOK_SECRET"].encode() # whsec_...
@app.post("/webhooks/selectwin")
def selectwin_webhook():
raw = request.get_data() # corpo bruto (bytes)
header = request.headers.get("X-Selectwin-Signature", "")
received = header[7:] if header.startswith("sha256=") else header
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected): # constant-time
abort(401)
event = request.get_json()
# ... enfileire e processe event["type"] / event["data"] ...
return "ok", 200PHP
<?php
$secret = getenv('SELECTWIN_WEBHOOK_SECRET'); // whsec_...
$raw = file_get_contents('php://input'); // corpo bruto
$header = $_SERVER['HTTP_X_SELECTWIN_SIGNATURE'] ?? '';
$received = str_starts_with($header, 'sha256=') ? substr($header, 7) : $header;
$expected = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $received)) { // constant-time
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($raw, true);
// ... enfileire e processe $event['type'] / $event['data'] ...
http_response_code(200);
echo 'ok';Reproduzindo a assinatura no terminal
Útil para depurar uma entrega: pegue o corpo bruto exato (ex.: salvo em payload.json) e recalcule o HMAC com o seu segredo. O resultado deve ser igual ao valor após sha256= no cabeçalho recebido.
# Calcula HMAC-SHA256(corpo, whsec_...) em hexadecimal
cat payload.json | openssl dgst -sha256 -hmac "$SELECTWIN_WEBHOOK_SECRET"
# Saída esperada (exemplo): SHA2-256(stdin)= 5d41402abc4b2a76b9719d911017c592...Nota
Use o arquivo bruto exato que chegou na requisição. Se você reformatar o JSON (com jq, por exemplo) antes de calcular, os bytes mudam e a assinatura não vai bater.
Como responder
A resposta da sua verificação é simples e não usa os error.code da API Selectwin — quem responde aqui é o seu servidor:
| Situação | Resposta do seu endpoint | Ação |
|---|---|---|
| Assinatura válida | 2xx (ex.: 200) | Enfileire e processe o evento |
| Assinatura inválida ou cabeçalho ausente | 401 | Descarte; não processe |
Evento já processado (mesmo id) | 2xx | Ignore (idempotência) e confirme o recebimento |
Os
error.codedo Catálogo de Códigos de Erro (ex.:invalidWebhookEventId,webhookEventIdInvalid) referem-se a chamadas que você faz à API de webhooks (listar/reenviar) — não à verificação de assinatura recebida. Veja Códigos de Erro.
Boas práticas
- Responda rápido (2xx) e processe de forma assíncrona. A Selectwin aguarda a resposta por até 15 segundos; depois disso, a entrega é cancelada e reagendada. Coloque o evento em uma fila e retorne
200imediatamente. - Trate reentregas como idempotentes. Retries podem entregar o mesmo evento mais de uma vez. Use o
iddo evento (wbh_...) para deduplicar — processe cadaidapenas uma vez. Veja Idempotência. - Verifique a assinatura ANTES de fazer parsing. Calcule o HMAC sobre o corpo bruto; só então converta o JSON.
- Compare em tempo constante. Use
timingSafeEqual/hmac.compare_digest/hash_equals, nunca==ou===sobre strings. - Nunca logue o segredo (
whsec_...). Trate-o como uma senha; mantenha-o em variável de ambiente ou cofre. - Endpoints sem segredo não recebem assinatura. Endpoints legados criados sem segredo não enviam o cabeçalho
X-Selectwin-Signature(compatibilidade retroativa). Recrie o endpoint para obter umwhsec_e passar a verificar assinaturas. - Rotação de segredo. Para rotacionar, crie um novo endpoint (com novo
whsec_), migre o tráfego e remova o antigo. Aceite ambas as assinaturas durante a janela de migração. - Exija HTTPS. Sirva seu endpoint apenas sobre TLS.
Veja também
- Webhook Events — Visão Geral — o que são os eventos e como a entrega funciona
- Catálogo de Eventos de Webhook — todos os
typeque você pode receber - Listar Eventos — auditar entregas e tentativas
- Reenviar Evento — disparar manualmente uma nova entrega
- Proibição de Polling — por que usar webhooks em vez de polling
- Idempotência — deduplicar reentregas
- Convenções da API — dinheiro em centavos, datas ISO 8601 UTC, IDs opacos
How is this guide?