Verificar a assinatura (HMAC)
A cada entrega, a Selectwin calcula uma assinatura HMAC-SHA256 sobre o corpo bruto (raw body) do
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 alterado em trânsito. Esta é uma etapa obrigatória para integrações em produção que movimentam dinheiro.
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 (POST /v1/webhooks→ camposecret) ou ao rotacioná-lo (POST /v1/webhooks/{webhookId}/rotate-secret). Guarde-o com segurança (variável de ambiente, cofre). - A mensagem assinada é exatamente o corpo JSON recebido. Para reproduzir a assinatura você precisa do corpo bruto — não re-serialize o JSON (reordenar chaves ou mudar espaçamento quebra a verificação).
Cabeçalhos enviados em cada entrega
| Cabeçalho | Conteúdo |
|---|---|
X-Selectwin-Signature | sha256=<hex> — HMAC-SHA256 do corpo bruto (cabeçalho principal a verificar) |
X-Selectwin-Signature-v1 | t=<unix>,v1=<hex> — esquema à prova de replay, HMAC-SHA256 de <t>.<rawBody> (opcional) |
X-Selectwin-Timestamp | O mesmo <unix> (segundos) presente no v1= |
X-Selectwin-Event | O tipo do evento (ex.: transaction.approved) |
X-Selectwin-Event-Id | O ID do evento (wbh_...) |
X-Selectwin-Delivery | O ID do dispatch desta tentativa (wdi_...) |
User-Agent | Selectwin-Webhook/1.0 |
Authorization | Presente apenas se você configurou headerAuthorization no endpoint (valor cru, sem prefixo de tipo) |
O
X-Selectwin-Signature(legadosha256=) é o cabeçalho principal e suficiente para validar a origem. OX-Selectwin-Signature-v1é oferecido adicionalmente para receptores que queiram proteção extra contra replay (vincula o timestamp à assinatura).
Passos da verificação
- Leia o corpo bruto da requisição (bytes/string exatos, antes de qualquer parsing JSON).
- Calcule
HMAC-SHA256(rawBody, endpointSecret)e represente em hexadecimal. - Compare com o valor após
sha256=no cabeçalhoX-Selectwin-Signature, usando uma comparação de tempo constante (evita ataques de timing). - Se não baterem (ou o cabeçalho estiver ausente), responda
401e não processe o evento.
⚠️ Use o corpo bruto. Frameworks que fazem
JSON.parseautomático e te entregam um objeto já alteraram a representação. Configure o parser para preservar o corpo bruto (ver exemplos abaixo).
Exemplos
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() aqui.
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
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'));
// ... 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()
# ... 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);
// ... processe $event['type'] / $event['data'] ...
http_response_code(200);
echo 'ok';Boas práticas
- Responda rápido (2xx) e processe de forma assíncrona. A Selectwin aguarda a resposta por até
30 segundos; depois disso, a entrega é registrada como falha e reagendada (backoff). 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. - Nunca logue o segredo (
whsec_...). Trate-o como uma senha. - Endpoints sem segredo não recebem assinatura. Endpoints legados criados sem segredo não enviam os
cabeçalhos de assinatura (compatibilidade retroativa). Rotacione o segredo
(
POST /v1/webhooks/{webhookId}/rotate-secret) para passar a receber assinaturas. - Rotação de segredo. Use
POST /v1/webhooks/{webhookId}/rotate-secretpara gerar um novowhsec_(retornado uma única vez). Durante a janela de troca, aceite a assinatura calculada com o segredo antigo e com o novo até confirmar que todas as entregas usam o novo. - Exija HTTPS. O campo
endpointjá exigehttps://; sirva seu endpoint apenas sobre TLS.
Veja também
- Webhook Events — Visão Geral
- Proibição de Polling — por que usar webhooks em vez de polling
- Idempotência
How is this guide?