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

# Implementar webhooks

> Recibe notificaciones en tiempo real sobre pagos, cargos, dispersiones y otros eventos.

## ¿Qué vas a lograr?

Al terminar esta guía tendrás un servidor que recibe y valida webhooks de OnePay, permitiéndote reaccionar en tiempo real a eventos como pagos aprobados, cargos exitosos o dispersiones completadas.

## Prerrequisitos

* Cuenta de OnePay con llaves API
* Un servidor web con una URL pública accesible (o [Ngrok](https://ngrok.com) para desarrollo local)

## ¿Cómo funcionan los webhooks?

```mermaid theme={null}
sequenceDiagram
    participant Cliente
    participant OnePay
    participant Tu Servidor

    Cliente->>OnePay: Realiza un pago
    OnePay->>Tu Servidor: POST (evento + firma HMAC)
    Tu Servidor->>Tu Servidor: Verifica firma
    Tu Servidor-->>OnePay: 200 OK
    Tu Servidor->>Tu Servidor: Procesa el evento
```

OnePay envía una solicitud HTTP POST a tu URL con:

* El payload del evento en el body
* Un header `x-webhook-token` con un token de autenticación
* Una firma HMAC-SHA256 para verificar la integridad

## Paso a paso

<Steps>
  <Step title="Configurar la URL del webhook">
    1. Ve a [Desarrolladores > Webhooks](https://admin.onepay.la/developer/webhooks) en el panel de OnePay
    2. Agrega la URL de tu servidor (ej: `https://tuapp.com/webhooks/onepay`)
    3. Copia el **secreto** generado y el **token de autenticación** - los necesitarás para verificar los webhooks
  </Step>

  <Step title="Crear el endpoint en tu servidor">
    Tu servidor debe:

    1. Recibir solicitudes POST
    2. Verificar la firma HMAC
    3. Responder `200 OK` inmediatamente
    4. Procesar el evento de forma asíncrona

    <Tabs>
      <Tab title="Node.js (Express)">
        ```javascript theme={null}
        const express = require('express');
        const crypto = require('crypto');

        const app = express();
        app.use(express.json());

        const WEBHOOK_SECRET = 'wh_tok_TU_SECRETO';
        const WEBHOOK_TOKEN = 'wh_hdr_TU_TOKEN';

        app.post('/webhooks/onepay', (req, res) => {
          // 1. Verificar el token de autenticación
          const token = req.headers['x-webhook-token'];
          if (token !== WEBHOOK_TOKEN) {
            return res.status(401).send('Token inválido');
          }

          // 2. Verificar la firma HMAC
          const payload = JSON.stringify(req.body);
          const signature = crypto
            .createHmac('sha256', WEBHOOK_SECRET)
            .update(payload)
            .digest('hex');

          // 3. Responder 200 inmediatamente
          res.status(200).send('OK');

          // 4. Procesar el evento
          const event = req.body.event;
          switch (event.type) {
            case 'payment.approved':
              console.log('Pago aprobado:', req.body.payment.id);
              // Actualizar orden en tu base de datos
              break;
            case 'cashout.processed':
              console.log('Dispersión procesada:', req.body.cashout.id);
              break;
            default:
              console.log('Evento recibido:', event.type);
          }
        });

        app.listen(3000);
        ```
      </Tab>

      <Tab title="Python (Flask)">
        ```python theme={null}
        import hmac
        import hashlib
        import json
        from flask import Flask, request

        app = Flask(__name__)

        WEBHOOK_SECRET = 'wh_tok_TU_SECRETO'
        WEBHOOK_TOKEN = 'wh_hdr_TU_TOKEN'

        @app.route('/webhooks/onepay', methods=['POST'])
        def handle_webhook():
            # 1. Verificar el token de autenticación
            token = request.headers.get('x-webhook-token')
            if token != WEBHOOK_TOKEN:
                return 'Token inválido', 401

            # 2. Verificar la firma HMAC
            payload = json.dumps(request.json, separators=(',', ':'))
            signature = hmac.new(
                WEBHOOK_SECRET.encode(),
                payload.encode(),
                hashlib.sha256
            ).hexdigest()

            # 3. Responder 200 inmediatamente
            # 4. Procesar el evento
            event = request.json.get('event', {})
            event_type = event.get('type')

            if event_type == 'payment.approved':
                payment = request.json.get('payment', {})
                print(f"Pago aprobado: {payment.get('id')}")
                # Actualizar orden en tu base de datos

            elif event_type == 'cashout.processed':
                cashout = request.json.get('cashout', {})
                print(f"Dispersión procesada: {cashout.get('id')}")

            return 'OK', 200

        if __name__ == '__main__':
            app.run(port=3000)
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Probar con Ngrok (desarrollo local)">
    Si estás desarrollando en local, usa Ngrok para exponer tu servidor:

    ```bash theme={null}
    ngrok http 3000
    ```

    Copia la URL generada (ej: `https://abc123.ngrok.io/webhooks/onepay`) y configúrala como URL del webhook en el panel de OnePay.
  </Step>
</Steps>

## Eventos disponibles

### Pagos

| Evento             | Descripción                              |
| ------------------ | ---------------------------------------- |
| `payment.created`  | Se creó una solicitud de pago            |
| `payment.approved` | El cliente completó el pago exitosamente |
| `payment.rejected` | El pago fue rechazado                    |
| `payment.expired`  | El link de pago expiró                   |
| `payment.deleted`  | Se eliminó la solicitud de pago          |

### Cargos (Débitos)

| Evento             | Descripción     |
| ------------------ | --------------- |
| `charge.succeeded` | Cargo exitoso   |
| `charge.declined`  | Cargo rechazado |

### Dispersiones

| Evento               | Descripción                        |
| -------------------- | ---------------------------------- |
| `cashout.created`    | Dispersión creada                  |
| `cashout.processing` | Dispersión en proceso              |
| `cashout.processed`  | Dispersión completada exitosamente |
| `cashout.cancelled`  | Dispersión cancelada               |
| `cashout.rejected`   | Dispersión rechazada               |

### Suscripciones

| Evento                   | Descripción                       |
| ------------------------ | --------------------------------- |
| `subscription.created`   | Suscripción creada                |
| `subscription.payment`   | Se generó un cobro de suscripción |
| `subscription.cancelled` | Suscripción cancelada             |

### Cuentas

| Evento             | Descripción                            |
| ------------------ | -------------------------------------- |
| `account.enrolled` | Cuenta bancaria vinculada exitosamente |
| `account.failed`   | Vinculación de cuenta falló            |

## Estructura del payload

Cada webhook tiene la siguiente estructura:

```json theme={null}
{
  "payment": {
    "id": "9e5ccd4a-d2f0-49dd-87fc-a0da752bd166",
    "amount": 150000,
    "status": "succeeded",
    ...
  },
  "event": {
    "type": "payment.approved",
    "timestamp": 1689262934,
    "environment": "test"
  }
}
```

El nombre del objeto principal varía según el tipo de evento (`payment`, `charge`, `cashout`, `subscription`, etc.).

## Buenas prácticas

<Warning>
  **Siempre** responde `200 OK` antes de procesar el evento. Si tu servidor tarda mucho en responder, OnePay podría considerar que el webhook falló e intentar reenviarlo.
</Warning>

1. **Verifica la firma**: Siempre valida el HMAC antes de procesar el evento
2. **Idempotencia**: Usa el `event.type` + el ID del recurso para evitar procesar un evento duplicado
3. **Responde rápido**: Retorna `200` inmediatamente y procesa el evento en segundo plano
4. **Registra los eventos**: Guarda un log de todos los webhooks recibidos para debugging
5. **Diferencia ambientes**: El campo `event.environment` indica si es `test` o `live`

## Errores comunes

| Problema           | Causa                                   | Solución                                                     |
| ------------------ | --------------------------------------- | ------------------------------------------------------------ |
| No recibo webhooks | URL incorrecta o inaccesible            | Verifica que tu URL sea pública y responda a POST            |
| Firma inválida     | Secreto incorrecto o payload modificado | Verifica que usas el secreto correcto y no modificas el body |
| Eventos duplicados | Tu servidor respondió con error         | Implementa idempotencia verificando el ID del evento         |
| Webhooks en local  | `localhost` no es accesible             | Usa Ngrok u otra herramienta de tunneling                    |

## Siguiente paso

<CardGroup cols={2}>
  <Card title="Cobrar con link de pago" icon="link" href="/guides/cobrar-link-pago">
    Crea tu primer cobro y recibe el webhook de pago aprobado.
  </Card>

  <Card title="Dispersar dinero" icon="money-bill-transfer" href="/guides/dispersar-dinero">
    Envía dinero y recibe el webhook de dispersión procesada.
  </Card>
</CardGroup>
