Webhook — test vectors
Use este vector para validar su función de verificación antes de conectar a producción. Si su implementación produce la firma esperada, puede confiar en su algoritmo.
Vector oficial
Sección titulada «Vector oficial»Confirmado por el equipo de backend. Aplicando hex(HMAC_SHA256(bytes.fromhex(secret), nonce + body)) sobre estos valores obtendrá exactamente la firma indicada.
secret_key (hex): 02d4b921007cad413e79731dd02b3267cd43a14d150a0ae6a1c651942122bb62
nonce: 1645634942
body (UTF-8 compacto, exactamente así — sin espacios envolventes):{"fiat_amount": 100.0, "fiat_currency": "USD", "status": "AC", "crypto_amount": 1.21461894, "unconfirmed_amount": 8.0, "confirmed_amount": 0.0, "currency": "DASH", "identifier": "1040095a-737d-41a2-a2e1-d031d19ec8cd"}
X-SIGNATURE: 395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2dScript de self-check
Sección titulada «Script de self-check»import crypto from 'node:crypto';
const secretHex = '02d4b921007cad413e79731dd02b3267cd43a14d150a0ae6a1c651942122bb62';const nonce = '1645634942';const body = '{"fiat_amount": 100.0, "fiat_currency": "USD", "status": "AC", "crypto_amount": 1.21461894, "unconfirmed_amount": 8.0, "confirmed_amount": 0.0, "currency": "DASH", "identifier": "1040095a-737d-41a2-a2e1-d031d19ec8cd"}';const expected = '395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2d';
const got = crypto.createHmac('sha256', Buffer.from(secretHex, 'hex')).update(nonce + body, 'utf8').digest('hex');
console.log(got === expected ? '✓ OK' : `✗ mismatch: ${got}`);import hmac, hashlib
secret_hex = '02d4b921007cad413e79731dd02b3267cd43a14d150a0ae6a1c651942122bb62'nonce = '1645634942'body = '{"fiat_amount": 100.0, "fiat_currency": "USD", "status": "AC", "crypto_amount": 1.21461894, "unconfirmed_amount": 8.0, "confirmed_amount": 0.0, "currency": "DASH", "identifier": "1040095a-737d-41a2-a2e1-d031d19ec8cd"}'expected = '395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2d'
key = bytes.fromhex(secret_hex)got = hmac.new(key, (nonce + body).encode('utf-8'), hashlib.sha256).hexdigest()
print('✓ OK' if got == expected else f'✗ mismatch: {got}')$secretHex = '02d4b921007cad413e79731dd02b3267cd43a14d150a0ae6a1c651942122bb62';$nonce = '1645634942';$body = '{"fiat_amount": 100.0, "fiat_currency": "USD", "status": "AC", "crypto_amount": 1.21461894, "unconfirmed_amount": 8.0, "confirmed_amount": 0.0, "currency": "DASH", "identifier": "1040095a-737d-41a2-a2e1-d031d19ec8cd"}';$expected = '395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2d';
$got = hash_hmac('sha256', $nonce . $body, hex2bin($secretHex));
echo ($got === $expected) ? "✓ OK\n" : "✗ mismatch: $got\n";package main
import ("crypto/hmac""crypto/sha256""encoding/hex""fmt")
func main() {secretHex := "02d4b921007cad413e79731dd02b3267cd43a14d150a0ae6a1c651942122bb62"nonce := "1645634942"body := `{"fiat_amount": 100.0, "fiat_currency": "USD", "status": "AC", "crypto_amount": 1.21461894, "unconfirmed_amount": 8.0, "confirmed_amount": 0.0, "currency": "DASH", "identifier": "1040095a-737d-41a2-a2e1-d031d19ec8cd"}`expected := "395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2d"
key, _ := hex.DecodeString(secretHex)mac := hmac.New(sha256.New, key)mac.Write([]byte(nonce + body))got := hex.EncodeToString(mac.Sum(nil))
if got == expected { fmt.Println("✓ OK")} else { fmt.Println("✗ mismatch:", got)}}Causas comunes de mismatch
Sección titulada «Causas comunes de mismatch»Si su resultado no coincide con 395a6c02…, revise en este orden:
- Secret decoding — ¿convirtió el hex a bytes? (
Buffer.from(hex, 'hex')en Node,bytes.fromhex(hex)en Python,hex2bin($hex)en PHP). - Body exact match — ¿usa el body crudo, byte a byte? Reserializar JSON cambia el whitespace y rompe la firma. El body debe leerse del stream HTTP como string/Buffer sin pasar por
JSON.parsey luegoJSON.stringify. - Formato del body — sin espacios envolventes entre
{y el primer campo, con,y:entre pares. - Concatenación —
nonce + bodyen ese orden, sin separadores ni trailing newline. - Charset — UTF-8.
- Output encoding — hex lowercase.
Ir más lejos
Sección titulada «Ir más lejos»- Con Claude / Cursor: instale el MCP server
@b4bit/b4bit-pay-mcp(npx @b4bit/b4bit-pay-mcp). Su toolverify_webhookreproduce este algoritmo con diagnósticos detallados cuando la firma no coincide (secret mal decodificado, concatenación inversa, whitespace envolvente). - En producción: capture un webhook real (usando ngrok o cloudflared) y verifique contra ese body concreto byte-exact. Es la validación definitiva antes de ir live.