Webhook — test vectors
Use this vector to validate your verification function before connecting to production. If your implementation produces the expected signature, you can trust your algorithm.
Official vector
Section titled “Official vector”Confirmed by the backend team. Applying hex(HMAC_SHA256(bytes.fromhex(secret), nonce + body)) on these values produces exactly the signature shown.
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: 395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2dSelf-check script
Section titled “Self-check script”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)}}Common causes of a mismatch
Section titled “Common causes of a mismatch”If your result does not match 395a6c02…, check in this order:
- Secret decoding — did you convert the hex to bytes? (
Buffer.from(hex, 'hex')in Node,bytes.fromhex(hex)in Python,hex2bin($hex)in PHP). - Body exact match — are you using the raw body, byte-for-byte? Re-serializing JSON changes the whitespace and breaks the signature. Read the body from the HTTP stream as a string/Buffer without passing it through
JSON.parsefollowed byJSON.stringify. - Body format — no surrounding spaces between
{and the first field, with,and:between pairs. - Concatenation —
nonce + bodyin that order, no separators or trailing newline. - Charset — UTF-8.
- Output encoding — hex lowercase.
Going further
Section titled “Going further”- With Claude / Cursor: install the MCP server
@b4bit/b4bit-pay-mcp(npx @b4bit/b4bit-pay-mcp). Itsverify_webhooktool reproduces this algorithm with detailed diagnostics when the signature does not match (bad secret decoding, reversed concatenation, surrounding whitespace). - In production: capture a real webhook (using ngrok or cloudflared) and verify against that specific body byte-exact. This is the final validation before going live.