Saltar al contenido principal
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

EventoCuá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_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).

Formato de entrega

Cada entrega es un POST JSON con estos headers:
HeaderContenido
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:
Intento12345
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.).
Última modificación el 12 de julio de 2026