Los webhooks te notifican los eventos de tu cuenta en un callback HTTPS
propio, firmados criptográficamente.
Crear una suscripción
curl -X POST https://api.qbank.cl/platform/v1/webhooks/subscriptions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"event_type": "payout_status_changed",
"callback_url": "https://api.miapp.com/webhooks/cbpay",
"secret": "un-secreto-de-al-menos-16-chars"
}'
event_type: uno de los eventos de la tabla siguiente, o * para todos.
callback_url: HTTPS obligatorio ; se rechazan localhost e IPs
privadas — para desarrollo local usa un
túnel HTTPS .
secret: mínimo 16 caracteres; se usa para firmar cada entrega. Se
almacena cifrado y no puede recuperarse.
La suscripción recibe los eventos de tu cuenta . Puedes listar las
suscripciones activas en cualquier momento:
curl https://api.qbank.cl/platform/v1/webhooks/subscriptions \
-H "Authorization: Bearer <token>"
{
"page" : 1 ,
"page_size" : 50 ,
"subscriptions" : [
{
"id" : "5f3a…" ,
"event_type" : "payout_status_changed" ,
"callback_url" : "https://api.miapp.com/webhooks/cbpay" ,
"status" : "active" ,
"created_at" : "2026-07-01T12:00:00Z"
}
]
}
Eventos
Evento Cuándo se emite payin_creditedUn cobro fiat fue recibido y abonado payout_status_changedUn payout cambió de estado transfer_receivedLa cuenta recibió una transferencia interna crypto_deposit_creditedUn depósito on-chain fue confirmado y abonado crypto_deposit_heldUn depósito entrante quedó retenido por riesgo del remitente (screening ) crypto_deposit_alertUn depósito se acreditó pero el remitente presenta riesgo alto (informativo) crypto_withdrawal_status_changedUn retiro on-chain cambió de estado banking_customer_status_changedCambió la verificación de tu perfil bancario banking_operation_status_changedUn pago bancario cambió de estado card_transactionUna compra con tarjeta fue autorizada, anulada o ajustada card_status_changedUna tarjeta cambió de estado (incluye congelamiento automático) kyc_verification_status_changed / kyb_verification_status_changedUna verificación de identidad cambió de estado (incluye tu propio onboarding, con self_onboarding: true) kyc_link_completed / kyb_link_completedUn link de verificación hosteado fue completado kyc_document_validated / kyb_document_validatedTerminó el OCR de un documento subido por API kyc_liveness_completedUna prueba de vida fue completada desde un liveness link aml_screening_updatedNovedades del screening AML (resultado, casos, riesgo, transacción revisada) wallet_deposit_receivedLlegó un depósito on-chain a una wallet segregada (no toca el ledger) wallet_send_status_changedUn envío desde una wallet segregada cambió de estado wallet_key_exportedSe exportó la llave privada de una wallet segregada (alerta de seguridad) wallet_external_movementMovimiento on-chain de una wallet segregada que no pasó por la plataforma (esperable en custodia client) wallet_key_compromise_suspectedAlarma crítica : salida externa desde una wallet con custodia cbpay — posible llave comprometida
Payload de cada evento
payin_credited
payout_status_changed
transfer_received
crypto_deposit_credited
crypto_deposit_held
crypto_deposit_alert
crypto_withdrawal_status_changed
banking_customer_status_changed
banking_operation_status_changed
card_transaction
card_status_changed
kyc_verification_status_changed
kyb_link_completed
kyc_document_validated
kyc_liveness_completed
aml_screening_updated
wallet_deposit_received
wallet_send_status_changed
wallet_key_exported
wallet_external_movement
wallet_key_compromise_suspected
{
"payin_id" : "9c2a…" ,
"account_id" : "ae8c…" ,
"country" : "BO" ,
"currency" : "BOB" ,
"local_amount" : "700.00" ,
"fx_rate" : "6.91" ,
"usdt_credited" : "100.302460" ,
"fee" : "1.000000"
}
{
"payout_id" : "0d4f…" ,
"account_id" : "ae8c…" ,
"country" : "MX" ,
"currency" : "MXN" ,
"local_amount" : "1500.00" ,
"usdt_amount" : "85.714286" ,
"total_debit" : "86.014286" ,
"status" : "completed" ,
"status_code" : ""
}
{
"transfer_id" : "77b1…" ,
"from_account_id" : "389d…" ,
"to_account_id" : "ae8c…" ,
"asset" : "USDT" ,
"amount" : "25.000000" ,
"description" : "Split de gastos" ,
"created_at" : "2026-07-06T20:10:00Z"
}
{
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"tx_id" : "b1946ac9…" ,
"amount" : "499.000000" ,
"fee" : "1.000000"
}
{
"account_id" : "ae8c…" ,
"hold_id" : "c1d2e3f4…" ,
"chain" : "tron" ,
"asset" : "usdt" ,
"tx_id" : "8a5b3c…" ,
"risk" : "Severe" ,
"status" : "held"
}
{
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "usdt" ,
"tx_id" : "9c6d4e…" ,
"risk" : "High" ,
"status" : "credited"
}
{
"withdrawal_id" : "5e8c…" ,
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"tx_id" : "7d3f01aa…" ,
"status" : "completed" ,
"amount" : "100.000000"
}
{
"account_id" : "ae8c…" ,
"customer_id" : "9f2b…" ,
"kyc_status" : "approved"
}
{
"account_id" : "ae8c…" ,
"customer_id" : "9f2b…" ,
"operation_id" : "7e8a…" ,
"type" : "withdraw" ,
"status" : "completed"
}
{
"account_id" : "ae8c…" ,
"card_id" : "3c2b…" ,
"transaction_id" : "5e4d…" ,
"status" : "authorized" ,
"amount_usdt" : "16.170000" ,
"merchant" : "AMZN Mktp"
}
{
"account_id" : "ae8c…" ,
"card_id" : "3c2b…" ,
"status" : "frozen" ,
"reason" : "monthly_fee_unpaid"
}
{
"account_id" : "ae8c…" ,
"kind" : "kyc" ,
"event" : "approved" ,
"submission_id" : "c3d4…" ,
"external_customer_id" : "cust_789" ,
"status" : "approved" ,
"risk_band" : "low" ,
"decision" : "approved"
}
{
"account_id" : "ae8c…" ,
"kind" : "kyb" ,
"event" : "link_completed" ,
"link_id" : "b2c3…" ,
"submission_id" : "d4e5…" ,
"external_customer_id" : "cust_456" ,
"status" : "completed"
}
{
"account_id" : "ae8c…" ,
"kind" : "kyc" ,
"submission_id" : "c3d4…" ,
"external_customer_id" : "cust_789" ,
"category" : "identity" ,
"outcome" : "MATCH" ,
"score" : 0.97 ,
"summary" : "Document matches the submitted identity"
}
{
"account_id" : "ae8c…" ,
"kind" : "kyc" ,
"submission_id" : "c3d4…" ,
"external_customer_id" : "cust_789" ,
"outcome" : "PASS" ,
"passed" : true
}
{
"account_id" : "ae8c…" ,
"screening_event" : "compliance_risk_changed" ,
"customer_id" : "cus_8f2e…" ,
"data" : { "risk_level" : "high" }
}
{
"wallet_id" : "b7e3…" ,
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"tx_id" : "b1946ac9…" ,
"amount_raw" : "125000000" ,
"from_address" : "TDonor…"
}
{
"send_id" : "9c8b…" ,
"wallet_id" : "b7e3…" ,
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"tx_id" : "b1946ac9…" ,
"status" : "completed" ,
"amount_raw" : "25500000"
}
{
"wallet_id" : "b7e3…" ,
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"address" : "TRmSZRaMAqLEevAdGwo3R43bRBXamWR5bd"
}
{
"wallet_id" : "b7e3…" ,
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"direction" : "out" ,
"tx_id" : "9a3c1e5f…" ,
"amount_raw" : "25000000" ,
"custody" : "client"
}
{
"wallet_id" : "b7e3…" ,
"account_id" : "ae8c…" ,
"chain" : "tron" ,
"asset" : "USDT" ,
"direction" : "out" ,
"tx_id" : "9a3c1e5f…" ,
"amount_raw" : "25000000" ,
"custody" : "cbpay"
}
En payout_status_changed y crypto_withdrawal_status_changed, status
puede ser completed o failed (con failed el débito ya fue
reembolsado cuando recibes el evento).
Cada entrega es un POST JSON con estos headers:
Header Contenido X-Webhook-EventTipo de evento X-Webhook-Event-IDID único del evento X-Webhook-Delivery-IDID de esta entrega (cambia entre reintentos) X-Webhook-TimestampUnix timestamp (segundos, UTC) X-Webhook-SignatureFirma HMAC (ver abajo)
Verificar la firma
X-Webhook-Signature = hex( HMAC-SHA256( secret, timestamp + "." + body ) )
const crypto = require ( "crypto" );
function verifyWebhook ( req , secret ) {
const ts = req.headers[ "x-webhook-timestamp" ];
const sig = req.headers[ "x-webhook-signature" ];
const expected = crypto
. createHmac ( "sha256" , secret)
. update (ts + "." + req.rawBody)
. digest ( "hex" );
return crypto. timingSafeEqual (Buffer. from (sig), Buffer. from (expected));
}
import hashlib, hmac
def verify_webhook (headers, raw_body: bytes , secret: str ) -> bool :
ts = headers[ "X-Webhook-Timestamp" ]
sig = headers[ "X-Webhook-Signature" ]
expected = hmac.new(
secret.encode(), f " { ts } ." .encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(sig, expected)
Calcula el HMAC sobre el body crudo (bytes tal como llegan), no sobre el
JSON re-serializado. Rechaza timestamps muy antiguos (> 5 minutos) para
prevenir replay.
Reintentos e idempotencia
Tu endpoint debe responder 2xx dentro del timeout; cualquier otra
respuesta se reintenta.
Hasta 5 intentos con backoff incremental:
Intento 1 2 3 4 5 Espera aprox. inmediato ~5s ~20s ~45s ~80s
Usa X-Webhook-Event-ID para deduplicar: el mismo evento puede llegar más
de una vez (entregas at-least-once).
Si los 5 intentos fallan, el evento no se reenvía — recupera el estado
con el GET del recurso (por eso ningún flujo debe depender SOLO del
webhook).
Buenas prácticas
Responde 200 de inmediato y procesa en background.
Registra el X-Webhook-Delivery-ID para trazabilidad.
No dependas solo de webhooks para estados críticos: puedes consultar el
objeto por API en cualquier momento (GET /v1/payouts/{id}, etc.).