> ## 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/developers/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();
        // El cuerpo SIN parsear: la firma se calcula sobre esos bytes exactos.
        app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

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

        function safeEqual(a, b) {
          const bufA = Buffer.from(a ?? '');
          const bufB = Buffer.from(b ?? '');
          return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB);
        }

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

          // 2. Verificar la firma HMAC contra el header `Signature`
          const expected = crypto
            .createHmac('sha256', WEBHOOK_SECRET)
            .update(req.rawBody)
            .digest('hex');

          if (!safeEqual(req.headers['signature'], expected)) {
            return res.status(401).send('Firma inválida');
          }

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

          // 4. Procesar el evento (en producción, encólalo y sal de la request)
          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.completed':
              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
        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 not hmac.compare_digest(token, WEBHOOK_TOKEN):
                return 'Token inválido', 401

            # 2. Verificar la firma HMAC contra el header `Signature`.
            #    Sobre el cuerpo SIN parsear: si lo reserializas, la firma no coincide.
            expected = hmac.new(
                WEBHOOK_SECRET.encode(),
                request.get_data(),
                hashlib.sha256
            ).hexdigest()

            if not hmac.compare_digest(request.headers.get('Signature', ''), expected):
                return 'Firma inválida', 401

            # 3. Encolar el evento y responder 200 de inmediato: el procesamiento
            #    no puede ocurrir dentro de la request (timeout de 10 s).
            encolar_evento(request.get_json())

            return 'OK', 200

        def encolar_evento(body):
            """Publica el evento en tu cola (Celery, RQ, SQS…) y procésalo en un worker."""
            event_type = body.get('event', {}).get('type')

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

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

        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.paid`   | Cargo exitoso   |
| `charge.failed` | Cargo rechazado |

### Dispersiones

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

### Suscripciones

| Evento                  | Descripción                         |
| ----------------------- | ----------------------------------- |
| `subscription.created`  | Suscripción creada                  |
| `subscription.paid`     | Se pagó una cuota de la suscripción |
| `subscription.canceled` | Suscripción cancelada               |

### Cuentas

| Evento                | Descripción                            |
| --------------------- | -------------------------------------- |
| `account.connected`   | Cuenta bancaria vinculada exitosamente |
| `account.pending`     | Cuenta pendiente de validación         |
| `account.uncompleted` | La vinculación quedó incompleta        |

Esta es una selección de los eventos más usados. El listado completo está en la [referencia de webhooks](/client/webhooks/index#eventos-disponibles).

## 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. El timeout de entrega es de **10
  segundos**: si tardas más, la entrega cuenta como fallida y OnePay la reintenta.
</Warning>

### Qué pasa si tu servidor falla

Si una entrega no responde `2xx`/`3xx` en 10 segundos, OnePay la reintenta espaciando los intentos
10 s, 30 s, 1 min, 2 min y 5 min: **6 intentos en total** —la entrega inicial y 5 reintentos— a lo
largo de unos 9 minutos. Un `4xx` de tu lado también dispara reintentos.

Además, dos procesos periódicos reenvían lo que quedó sin entregar, **con reglas distintas entre
sí**: la reconciliación da por terminal cualquier respuesta menor que `500` (un `4xx` tuyo cierra el
evento), mientras que el reenvío masivo solo excluye `2xx`/`3xx`, así que un evento que solo recibió
`4xx` puede volver a llegarte. Un endpoint que falla de forma sostenida se desactiva solo.

El detalle completo —calendario, reenvío automático, desactivación y cómo reactivar el endpoint—
está en [Entrega y reintentos](/client/webhooks/index#entrega-y-reintentos).

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. Los reintentos y los reenvíos automáticos hacen que recibir el mismo evento dos veces sea normal, no una excepción
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>
