> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cbpayapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recibe eventos firmados en tiempo real

Los webhooks te notifican los eventos de tu cuenta en un callback HTTPS
propio, firmados criptográficamente.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    autonumber
    participant CB as CBPay
    participant App as Tu endpoint HTTPS
    CB->>App: POST firmado (X-Webhook-Signature, X-Webhook-Event-ID)
    alt Respondes 2xx a tiempo
        App-->>CB: 200 OK
        Note over CB: Entrega completa
    else Timeout o error
        App-->>CB: 5xx / timeout
        CB->>App: Reintento con backoff (hasta 5 intentos)
        Note over App: El mismo evento puede llegar 2 veces —<br/>deduplica por X-Webhook-Event-ID
    end
```

## Crear una suscripción

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
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](/es/entorno-y-pruebas#probar-webhooks-en-desarrollo-local).
* `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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://api.qbank.cl/platform/v1/webhooks/subscriptions \
  -H "Authorization: Bearer <token>"
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "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_credited`                                                      | Un cobro fiat fue recibido y abonado                                                                       |
| `payin_expired`                                                       | Un cobro activo (QR / checkout) venció o falló sin recibir el pago                                         |
| `payout_status_changed`                                               | Un payout cambió de estado                                                                                 |
| `transfer_received`                                                   | La cuenta recibió una transferencia interna                                                                |
| `crypto_deposit_credited`                                             | Un depósito on-chain fue confirmado y abonado                                                              |
| `crypto_deposit_held`                                                 | Un depósito entrante quedó retenido por riesgo del remitente ([screening](/es/guias/screenings))           |
| `crypto_deposit_alert`                                                | Un depósito se acreditó pero el remitente presenta riesgo alto (informativo)                               |
| `crypto_withdrawal_status_changed`                                    | Un retiro on-chain cambió de estado                                                                        |
| `banking_customer_status_changed`                                     | Cambió la verificación de tu perfil bancario                                                               |
| `banking_operation_status_changed`                                    | Un pago bancario cambió de estado                                                                          |
| `card_transaction`                                                    | Una compra con tarjeta fue autorizada, anulada o ajustada                                                  |
| `card_status_changed`                                                 | Una tarjeta cambió de estado (incluye congelamiento automático)                                            |
| `kyc_verification_status_changed` / `kyb_verification_status_changed` | Una verificación de identidad cambió de estado (incluye tu propio onboarding, con `self_onboarding: true`) |
| `kyc_link_completed` / `kyb_link_completed`                           | Un link de verificación hosteado fue completado                                                            |
| `kyc_document_validated` / `kyb_document_validated`                   | Terminó el OCR de un documento subido por API                                                              |
| `kyc_liveness_completed`                                              | Una prueba de vida fue completada desde un liveness link                                                   |
| `aml_screening_updated`                                               | Novedades del screening AML (resultado, casos, riesgo, transacción revisada)                               |
| `wallet_deposit_received`                                             | Llegó un depósito on-chain a una [wallet segregada](/es/guias/wallets-segregadas) (no toca el ledger)      |
| `wallet_send_status_changed`                                          | Un envío desde una wallet segregada cambió de estado                                                       |
| `wallet_key_exported`                                                 | Se exportó la llave privada de una wallet segregada (alerta de seguridad)                                  |
| `wallet_external_movement`                                            | Movimiento on-chain de una wallet segregada que no pasó por la plataforma (esperable en custodia `client`) |
| `wallet_key_compromise_suspected`                                     | **Alarma crítica**: salida externa desde una wallet con custodia `cbpay` — posible llave comprometida      |

### Payload de cada evento

<CodeGroup>
  ```json payin_credited theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "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"
  }
  ```

  ```json payin_expired theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "payin_id": "567d…",
    "account_id": "ae8c…",
    "status": "expired",
    "country": "BO",
    "currency": "BOB",
    "local_amount": "60.99",
    "reference": "CBK7Q2M4XZ9P"
  }
  ```

  ```json payout_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "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": ""
  }
  ```

  ```json transfer_received theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "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"
  }
  ```

  ```json crypto_deposit_credited theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "tx_id": "b1946ac9…",
    "amount": "499.000000",
    "fee": "1.000000"
  }
  ```

  ```json crypto_deposit_held theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "hold_id": "c1d2e3f4…",
    "chain": "tron",
    "asset": "usdt",
    "tx_id": "8a5b3c…",
    "risk": "Severe",
    "status": "held"
  }
  ```

  ```json crypto_deposit_alert theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "usdt",
    "tx_id": "9c6d4e…",
    "risk": "High",
    "status": "credited"
  }
  ```

  ```json crypto_withdrawal_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "withdrawal_id": "5e8c…",
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "tx_id": "7d3f01aa…",
    "status": "completed",
    "amount": "100.000000"
  }
  ```

  ```json banking_customer_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "customer_id": "9f2b…",
    "kyc_status": "approved"
  }
  ```

  ```json banking_operation_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "customer_id": "9f2b…",
    "operation_id": "7e8a…",
    "type": "withdraw",
    "status": "completed"
  }
  ```

  ```json card_transaction theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "card_id": "3c2b…",
    "transaction_id": "5e4d…",
    "status": "authorized",
    "amount_usdt": "16.170000",
    "merchant": "AMZN Mktp"
  }
  ```

  ```json card_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "card_id": "3c2b…",
    "status": "frozen",
    "reason": "monthly_fee_unpaid"
  }
  ```

  ```json kyc_verification_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "kind": "kyc",
    "event": "approved",
    "submission_id": "c3d4…",
    "external_customer_id": "cust_789",
    "status": "approved",
    "risk_band": "low",
    "decision": "approved"
  }
  ```

  ```json kyb_link_completed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "kind": "kyb",
    "event": "link_completed",
    "link_id": "b2c3…",
    "submission_id": "d4e5…",
    "external_customer_id": "cust_456",
    "status": "completed"
  }
  ```

  ```json kyc_document_validated theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "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"
  }
  ```

  ```json kyc_liveness_completed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "kind": "kyc",
    "submission_id": "c3d4…",
    "external_customer_id": "cust_789",
    "outcome": "PASS",
    "passed": true
  }
  ```

  ```json aml_screening_updated theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "account_id": "ae8c…",
    "screening_event": "compliance_risk_changed",
    "customer_id": "cus_8f2e…",
    "data": { "risk_level": "high" }
  }
  ```

  ```json wallet_deposit_received theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "wallet_id": "b7e3…",
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "tx_id": "b1946ac9…",
    "amount_raw": "125000000",
    "from_address": "TDonor…"
  }
  ```

  ```json wallet_send_status_changed theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "send_id": "9c8b…",
    "wallet_id": "b7e3…",
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "tx_id": "b1946ac9…",
    "status": "completed",
    "amount_raw": "25500000"
  }
  ```

  ```json wallet_key_exported theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "wallet_id": "b7e3…",
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "address": "TRmSZRaMAqLEevAdGwo3R43bRBXamWR5bd"
  }
  ```

  ```json wallet_external_movement theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "wallet_id": "b7e3…",
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "direction": "out",
    "tx_id": "9a3c1e5f…",
    "amount_raw": "25000000",
    "custody": "client"
  }
  ```

  ```json wallet_key_compromise_suspected theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "wallet_id": "b7e3…",
    "account_id": "ae8c…",
    "chain": "tron",
    "asset": "USDT",
    "direction": "out",
    "tx_id": "9a3c1e5f…",
    "amount_raw": "25000000",
    "custody": "cbpay"
  }
  ```
</CodeGroup>

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:

| Header                  | Contenido                                    |
| ----------------------- | -------------------------------------------- |
| `X-Webhook-Event`       | Tipo de evento                               |
| `X-Webhook-Event-ID`    | ID único del evento                          |
| `X-Webhook-Delivery-ID` | ID de esta entrega (cambia entre reintentos) |
| `X-Webhook-Timestamp`   | Unix timestamp (segundos, UTC)               |
| `X-Webhook-Signature`   | Firma HMAC (ver abajo)                       |

## Verificar la firma

```
X-Webhook-Signature = hex( HMAC-SHA256( secret, timestamp + "." + body ) )
```

<CodeGroup>
  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  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));
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  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)
  ```
</CodeGroup>

<Warning>
  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.
</Warning>

## 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.).
