SelectwinSelectwin

Verificando Assinaturas de Webhook

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:

HTTP
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 → campo secret) 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çalhoConteúdo
X-Selectwin-Signaturesha256=<hex> — HMAC-SHA256 do corpo bruto (cabeçalho principal a verificar)
X-Selectwin-Signature-v1t=<unix>,v1=<hex> — esquema à prova de replay, HMAC-SHA256 de <t>.<rawBody> (opcional)
X-Selectwin-TimestampO mesmo <unix> (segundos) presente no v1=
X-Selectwin-Signature-PreviousSó nas 24 h depois de uma troca do segredo: sha256=<hex>, calculado com o segredo anterior (veja abaixo)
X-Selectwin-Signature-v1-PreviousSó nas 24 h depois de uma troca do segredo: t=<unix>,v1=<hex>, com o segredo anterior
X-Selectwin-EventO tipo do evento (ex.: transaction.approved)
X-Selectwin-Event-IdO ID do evento (wbh_...)
X-Selectwin-DeliveryO ID do dispatch desta tentativa (wdi_...)
User-AgentSelectwin-Webhook/1.0
AuthorizationPresente apenas se você configurou headerAuthorization no endpoint (valor cru, sem prefixo de tipo)
sequenceDiagram
    autonumber
    participant SW as Selectwin
    participant EP as Seu Endpoint
    Note over SW: assina = HMAC_SHA256(rawBody, whsec_…)
    SW->>EP: POST evento + X-Selectwin-Signature: sha256=assina
    Note over EP: recalcula HMAC_SHA256(rawBody, whsec_…)
    alt assinaturas batem (comparação constant-time)
        EP-->>SW: 200 OK · processa o evento
    else não batem / ausente
        EP-->>SW: 401 · descarta o evento
    end

Passos da verificação

  1. Leia o corpo bruto da requisição (bytes/string exatos, antes de qualquer parsing JSON).
  2. Calcule HMAC-SHA256(rawBody, endpointSecret) e represente em hexadecimal.
  3. Compare com o valor após sha256= no cabeçalho X-Selectwin-Signature, usando uma comparação de tempo constante (evita ataques de timing). Nas 24 h depois de uma troca do segredo, compare também com o X-Selectwin-Signature-Previous (veja abaixo).
  4. Se não baterem (ou o cabeçalho estiver ausente), responda 401 e não processe o evento.

Durante a troca do segredo

Ao rotacionar o segredo (POST /v1/webhooks/{webhookId}/rotate-secret), o segredo novo volta uma única vez, na resposta, e o anterior continua valendo por 24 horas. Nessa janela, cada entrega vai assinada com os dois:

CabeçalhosAssinados com
X-Selectwin-Signature · X-Selectwin-Signature-v1o segredo novo
X-Selectwin-Signature-Previous · X-Selectwin-Signature-v1-Previouso segredo anterior, no mesmo formato (sha256=<hex> e t=<unix>,v1=<hex>)

Assim o seu receptor não perde entrega enquanto você troca a chave: com o segredo anterior, a assinatura bate com o -Previous; com o novo, bate com o cabeçalho principal. Para cobrir os dois casos sem coordenar o deploy, aceite a entrega quando o HMAC calculado com o segredo que você tem bater com o X-Selectwin-Signature ou com o X-Selectwin-Signature-Previous.

  • Passadas as 24 horas, os cabeçalhos -Previous deixam de vir e só o segredo novo assina.
  • Rotacionar de novo dentro da janela encerra na hora o segredo mais antigo: o anterior passa a ser o que acabou de ser trocado, com 24 horas novas.
  • Um endpoint que ainda não tinha segredo não recebe os cabeçalhos -Previous na primeira rotação: não há segredo anterior para assinar.

Exemplos

Node.js (Express)

JavaScript
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.payload.object ...
  res.status(200).send('ok');
});

Python (Flask)

Python
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["payload"]["object"] ...
    return "ok", 200

PHP

PHP
<?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['payload']['object'] ...
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 200 imediatamente.
  • Trate reentregas como idempotentes. Retries podem entregar o mesmo evento mais de uma vez. Use o id do evento (wbh_...) para deduplicar — processe cada id apenas 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-secret para gerar um novo whsec_ (retornado uma única vez). Por 24 horas, as entregas trazem também a assinatura com o segredo anterior, nos cabeçalhos -Previous: troque a chave no seu receptor dentro dessa janela (veja acima).
  • Exija HTTPS. O campo endpoint já exige https://; sirva seu endpoint apenas sobre TLS.

Veja também

Atualizado em 21 de outubro de 2026

Preferências de cookies

Hoje o site usa só cookies essenciais: nenhuma ferramenta de medição ou de anúncios está ligada, e nada é compartilhado com terceiros. Se isso mudar, você decide aqui antes de qualquer coisa ser carregada.

  • Mantêm o site funcionando: segurança, a sua escolha sobre cookies e preferências como a versão da documentação. Não dependem de consentimento.

    Sempre ativos