Webhook
El sistema de notificación por webhook le alerta en tiempo real de los cambios en el estado de un pago. Interpretarlos correctamente es crítico para evitar liberar producto sin confirmación o dejar a un cliente esperando.
Configuración
Sección titulada «Configuración»- URL del webhook: la configura en el dashboard, en la configuración del dispositivo (
notification_url). - Nivel: los webhooks operan por dispositivo. Cada dispositivo puede tener su propia URL.
- Método HTTP:
POST. - Body: JSON.
Headers
Sección titulada «Headers»POST /su/endpoint HTTP/1.1Content-Type: application/jsonX-NONCE: 1645634942X-SIGNATURE: 395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2dX-NONCE: Unix timestamp en segundos en el momento en que B4bit Pay generó la firma.X-SIGNATURE:hex(HMAC_SHA256(bytes.fromhex(secret_key), nonce_ascii + body_utf8)).
Algoritmo de firma
Sección titulada «Algoritmo de firma»firma = hex(HMAC_SHA256(secret_hex → bytes, nonce + body))- La Secret Key del dispositivo está en el dashboard en formato hexadecimal; decodifíquela a bytes antes de usarla.
noncees la cadena ASCII del timestamp ("1645634942", no el número entero).bodyes el cuerpo crudo de la petición, sin parsear ni reformatear (sin trailing newline adicional).- La concatenación es
nonce + bodysin separadores.
Ventana temporal
Sección titulada «Ventana temporal»Estructura del payload
Sección titulada «Estructura del payload»{ "fiat_amount": 5.0, "fiat_currency": "USD", "status": "AC", "crypto_amount": 0.06519511, "unconfirmed_amount": 0.77, "confirmed_amount": 0.0, "currency": "DASH", "identifier": "cc80e0b5-f779-4094-be65-fcee4b5bd041"}Estados críticos
Sección titulada «Estados críticos»ACconsafe=true→ pago recibido, estable, puede liberar.CO→ pago completado definitivamente.OC→ Out of Condition: recibido menor que esperado (no libere).EX,CA→ expirado o cancelado.
Ver estados de pago para la lista completa.
Cómo verificar la firma
Sección titulada «Cómo verificar la firma»import crypto from 'node:crypto';
export function verify(secretHex, nonce, bodyRaw, signatureHex) {const key = Buffer.from(secretHex, 'hex');const mac = crypto .createHmac('sha256', key) .update(nonce + bodyRaw, 'utf8') .digest('hex');const a = Buffer.from(mac);const b = Buffer.from(signatureHex);return a.length === b.length && crypto.timingSafeEqual(a, b);}import hmac, hashlib
def verify(secret_hex: str, nonce: str, body_raw: str, signature_hex: str) -> bool: key = bytes.fromhex(secret_hex) mac = hmac.new(key, (nonce + body_raw).encode('utf-8'), hashlib.sha256).hexdigest() return hmac.compare_digest(mac, signature_hex)function verify(string $secretHex, string $nonce, string $bodyRaw, string $signatureHex): bool { $key = hex2bin($secretHex); $mac = hash_hmac('sha256', $nonce . $bodyRaw, $key); return hash_equals($mac, $signatureHex);}package webhook
import ( "crypto/hmac" "crypto/sha256" "encoding/hex")
func Verify(secretHex, nonce, bodyRaw, signatureHex string) bool { key, err := hex.DecodeString(secretHex) if err != nil { return false } mac := hmac.New(sha256.New, key) mac.Write([]byte(nonce + bodyRaw)) want := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(want), []byte(signatureHex))}Test vector oficial
Sección titulada «Test vector oficial»Use estos valores para verificar que su implementación produce la misma firma que B4bit Pay. Si obtiene un resultado distinto, revise encoding, trailing newline, decodificación del secret o concatenación.
secret_key (hex): 02d4b921007cad413e79731dd02b3267cd43a14d150a0ae6a1c651942122bb62
nonce: 1645634942
body (UTF-8 compacto, exactamente así):{"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 esperado: 395a6c0294f0896fcc0e5827e926e12308f4fdca5c18da69d3af6879e5c80e2dVer self-checks en Node/Python/PHP/Go en Webhook — test vectors.
Respuesta esperada
Sección titulada «Respuesta esperada»Su servidor debe responder HTTP 2xx (200/201/202/204) rápidamente — idealmente en menos de 5 segundos. Procese la lógica pesada de forma asíncrona.
Política de reintentos
Sección titulada «Política de reintentos»Idempotencia
Sección titulada «Idempotencia»Aunque el backend no reintenta, trate cada webhook como potencialmente repetido (por retries de red a nivel TCP, deployments del merchant con duplicación temporal, etc.). Desduplique por identifier + status + edited_at antes de aplicar side effects.
Siguientes pasos
Sección titulada «Siguientes pasos»- Webhook — test vectors — 4 casos reproducibles.
- Webhook — ordering — garantías de orden y duplicados.
- Estados de pago — NR, PE, AC, IA, CO, CA, EX, OC, FA, DE, CM.