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

# Webhooks

> Recibe notificaciones en tiempo real cuando ocurren eventos en tu cuenta de OnePay.

Los **webhooks** te permiten recibir notificaciones HTTP automáticas cuando ocurren eventos en tu cuenta (pagos aprobados, dispersiones procesadas, cargos fallidos, etc.), sin necesidad de consultar la API constantemente.

## ¿Cómo funcionan?

<Steps>
  <Step title="Registra una URL">
    Crea un webhook con la URL de tu servidor y los eventos que quieres escuchar.
  </Step>

  <Step title="OnePay envía el evento">
    Cada vez que ocurra un evento seleccionado, OnePay enviará un `POST` a tu URL con el payload del evento en formato JSON.
  </Step>

  <Step title="Responde con 200">
    Tu servidor debe responder con un código HTTP `200` en menos de 30 segundos. Si no, OnePay reintentará la entrega.
  </Step>

  <Step title="Verifica la firma">
    Valida el header `x-onepay-signature` con tu `secret` para asegurarte de que la petición proviene de OnePay.
  </Step>
</Steps>

## Autenticación de los eventos

Cada entrega llega con estos headers:

<ResponseField name="x-webhook-token" type="string">
  El token estático que definiste al crear el webhook (campo `header`). Se envía siempre.
</ResponseField>

<ResponseField name="x-webhook-event" type="string">
  El tipo de evento, por ejemplo `charge.paid`. Se envía siempre.
</ResponseField>

<ResponseField name="Signature" type="string">
  HMAC-SHA256 en hexadecimal del cuerpo JSON, calculado con tu `secret`. **Solo se envía si el
  webhook tiene un `secret` configurado**; si lo creaste sin `secret`, las entregas no van firmadas.
</ResponseField>

<Warning>
  Versiones anteriores de esta página decían que la firma viajaba en `x-onepay-signature`. Ese
  header **no existe**: la firma va en `Signature` y el token estático en `x-webhook-token`. Si
  implementaste la verificación contra `x-onepay-signature`, nunca encontró el header y es probable
  que tu endpoint esté procesando eventos sin autenticar.
</Warning>

Valida **siempre** con comparación en tiempo constante, y rechaza la petición si no coincide.

```javascript JavaScript theme={null}
const crypto = require('crypto');

// `rawBody` debe ser el cuerpo sin parsear: si lo reserializas, la firma no coincide.
function verifyWebhook(req, rawBody, { token, secret }) {
  const sentToken = req.headers['x-webhook-token'] ?? '';
  const tokenOk = crypto.timingSafeEqual(Buffer.from(sentToken), Buffer.from(token));
  if (!tokenOk) return false;

  if (!secret) return true; // webhook sin secret: solo hay token

  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const sent = req.headers['signature'] ?? '';
  return sent.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected));
}
```

```python Python theme={null}
import hmac, hashlib

def verify_webhook(headers, raw_body: bytes, token: str, secret: str | None) -> bool:
    if not hmac.compare_digest(headers.get('x-webhook-token', ''), token):
        return False

    if not secret:
        return True  # webhook sin secret: solo hay token

    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(headers.get('Signature', ''), expected)
```

```php PHP theme={null}
<?php
function verifyWebhook(array $headers, string $rawBody, string $token, ?string $secret): bool
{
    if (! hash_equals($headers['x-webhook-token'] ?? '', $token)) {
        return false;
    }

    if ($secret === null) {
        return true; // webhook sin secret: solo hay token
    }

    $expected = hash_hmac('sha256', $rawBody, $secret);

    return hash_equals($headers['Signature'] ?? '', $expected);
}
```

```bash cURL theme={null}
# Así se ve una entrega entrante:
POST /tu-endpoint HTTP/1.1
x-webhook-token: el-token-que-definiste
x-webhook-event: charge.paid
Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Content-Type: application/json
```

<Warning>
  Guarda tu `secret` de forma segura. Solo se muestra una vez al crear el webhook. Si lo pierdes, deberás eliminarlo y crear uno nuevo.
</Warning>

## Eventos disponibles

### Pagos

| Evento                      | Descripción                   |
| --------------------------- | ----------------------------- |
| `payment.created`           | Pago creado                   |
| `payment.approved`          | Pago aprobado                 |
| `payment.rejected`          | Pago rechazado                |
| `payment.refunded`          | Pago devuelto                 |
| `payment.expired`           | Pago expirado                 |
| `payment.partially_paid`    | Pago parcialmente completado  |
| `payment.partial_expired`   | Pago parcial expirado         |
| `payment.issue`             | Incidencia en el pago         |
| `payment.disputed`          | Pago disputado                |
| `payment.message.delivered` | Mensaje de pago entregado     |
| `payment.message.failed`    | Mensaje de pago fallido       |
| `payment.message.replied`   | Mensaje de pago con respuesta |
| `payment.deleted`           | Pago eliminado                |

### Cargos

| Evento              | Descripción               |
| ------------------- | ------------------------- |
| `charge.created`    | Cargo creado              |
| `charge.processing` | Cargo en procesamiento    |
| `charge.paid`       | Cargo pagado exitosamente |
| `charge.failed`     | Cargo fallido             |
| `charge.refunded`   | Cargo devuelto            |
| `charge.disputed`   | Cargo disputado           |

### Dispersiones

| Evento                     | Descripción                    |
| -------------------------- | ------------------------------ |
| `cashout.created`          | Dispersión creada              |
| `cashout.processing`       | Dispersión en procesamiento    |
| `cashout.completed`        | Dispersión completada          |
| `cashout.rejected`         | Dispersión rechazada           |
| `cashout.cancelled`        | Dispersión cancelada           |
| `cashout.require_approval` | Dispersión requiere aprobación |
| `cashout.turbo_onboarding` | Onboarding de dispersión turbo |

### Suscripciones

| Evento                  | Descripción                 |
| ----------------------- | --------------------------- |
| `subscription.created`  | Suscripción creada          |
| `subscription.active`   | Suscripción activada        |
| `subscription.paid`     | Cuota de suscripción pagada |
| `subscription.pass_due` | Suscripción vencida         |
| `subscription.unpaid`   | Suscripción sin pagar       |
| `subscription.canceled` | Suscripción cancelada       |
| `subscription.finished` | Suscripción finalizada      |
| `subscription.frozen`   | Suscripción congelada       |

### Cuentas bancarias

| Evento                 | Descripción                    |
| ---------------------- | ------------------------------ |
| `account.pending`      | Cuenta pendiente de validación |
| `account.connected`    | Cuenta conectada exitosamente  |
| `account.disconnected` | Cuenta desconectada            |
| `account.validated`    | Cuenta validada                |
| `account.uncompleted`  | Cuenta incompleta              |

### Facturas

| Evento             | Descripción       |
| ------------------ | ----------------- |
| `invoice.created`  | Factura creada    |
| `invoice.paid`     | Factura pagada    |
| `invoice.canceled` | Factura cancelada |

### Facturación electrónica (DIAN)

El timbrado es asíncrono: estos eventos son la forma de enterarte del resultado. Ver [Facturas electrónicas](/client/electronic-invoices/index).

| Evento                       | Descripción                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------ |
| `invoice.stamped`            | La DIAN aceptó la factura. Trae el número, el CUFE y las URLs del XML y el PDF |
| `invoice.stamp_rejected`     | La DIAN rechazó el documento. Trae el motivo                                   |
| `invoice.credit_note_issued` | Se emitió una nota crédito sobre una factura                                   |

### Otros

| Evento                       | Descripción                        |
| ---------------------------- | ---------------------------------- |
| `utility.paid`               | Pago de servicio público aprobado  |
| `utility.rejected`           | Pago de servicio público rechazado |
| `connect_link.completed`     | Link de conexión completado        |
| `balance.updated`            | Saldo actualizado                  |
| `wallet.transaction.created` | Transacción de wallet creada       |

## Endpoints

<CardGroup cols={2}>
  <Card title="Listar webhooks" icon="list" href="/client/webhooks/list">
    Consulta todos los webhooks registrados en tu cuenta.
  </Card>

  <Card title="Ver webhook" icon="eye" href="/client/webhooks/detail">
    Obtén el detalle de un webhook específico.
  </Card>

  <Card title="Crear webhook" icon="plus" href="/client/webhooks/create">
    Registra una nueva URL y selecciona los eventos a escuchar.
  </Card>

  <Card title="Actualizar webhook" icon="pen" href="/client/webhooks/update">
    Modifica la URL, nombre o eventos de un webhook existente.
  </Card>

  <Card title="Eliminar webhook" icon="trash" href="/client/webhooks/delete">
    Elimina un webhook de tu cuenta.
  </Card>

  <Card title="Ejemplos de payloads" icon="code" href="/client/webhooks/ejemplos">
    Revisa la estructura de cada evento que recibirás.
  </Card>
</CardGroup>
