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

# Webhooks

> Las notificaciones que TapiPay envía a tu backend cuando algo cambia en un cobro.

Un webhook es una llamada `POST` que TapiPay hace a un endpoint **tuyo** cuando pasa algo que te importa: un pago se confirma, un cobro se rechaza o un cargo se revierte. Así te enteras al momento, sin consultar la API en loop.

<Note>
  Los webhooks **no los emite la API**, que no los implementa. Los emiten las plataformas de cobro de TapiPay, y se configuran **durante el onboarding**: no hay endpoint para darlos de alta ni para cambiar la URL.
</Note>

## Los eventos

| Evento                                                                              | `type`                           |
| ----------------------------------------------------------------------------------- | -------------------------------- |
| [Notificación de pago](/es/webhooks/notificacion-de-pago)                           | `SERVICE` o `REFERENCED-PAYMENT` |
| [Débito bancario fallido](/es/webhooks/debito-fallido)                              | `AUTO_DEBIT_FAILED`              |
| [Contracargo de débito bancario](/es/webhooks/contracargo-de-debito)                | `AUTO_DEBIT_CHARGEBACK`          |
| [Domiciliación con tarjeta fallida](/es/webhooks/domiciliacion-con-tarjeta-fallida) | `CARD_AUTOPAY_FAILED`            |
| [Pago con tarjeta fallido](/es/webhooks/pago-con-tarjeta-fallido)                   | `CARD_PAYMENT_STATUS`            |
| [Contracargo de tarjeta](/es/webhooks/contracargo-de-tarjeta)                       | `CARD_PAYMENT_CHARGEBACK`        |

<Warning>
  **Enruta por el campo `type`.** Todos los eventos llegan al mismo endpoint tuyo, así que `type` es lo que te dice cuál es cuál. No infieras el evento por los campos que trae el payload.

  Con una salvedad: en la notificación de pago, `type` describe **qué se cobró**, no qué pasó. Lo que te dice si ese pago se confirmó, falló o se revirtió es su campo `status`. En los otros cinco, en cambio, el `type` ya identifica el hecho.
</Warning>

<Note>
  La [notificación de pago](/es/webhooks/notificacion-de-pago) viene de la plataforma de pago referenciado y tiene su propio contrato, distinto del resto: su `status` va en minúsculas, su `type` no identifica el evento sino el tipo de operación, y su configuración se documenta en esa misma página. Los otros cinco comparten todo lo que sigue.
</Note>

## Fallo y contracargo no son lo mismo

Los cinco eventos de esta familia se dividen en dos grupos, y conviene no confundirlos: dicen cosas distintas sobre el mismo cobro.

<CardGroup cols={2}>
  <Card title="Fallo" icon="triangle-exclamation">
    El cobro nunca se concretó. Llegan con `status: "FAILED"`.
  </Card>

  <Card title="Contracargo" icon="rotate">
    El cobro se había concretado y después se revirtió. No traen `status`, porque no describen el estado de un pago sino un hecho posterior sobre un cargo ya procesado.
  </Card>
</CardGroup>

## Campos comunes

Los cinco eventos comparten esta base:

| Campo         | Tipo           | Descripción                                                                                                    |
| ------------- | -------------- | -------------------------------------------------------------------------------------------------------------- |
| `type`        | String         | Qué evento es. Úsalo para enrutar.                                                                             |
| `operationId` | String         | Identificador único de la operación. **Úsalo como llave de idempotencia.**                                     |
| `amount`      | Number         | Monto involucrado.                                                                                             |
| `companyCode` | String \| null | Código de la empresa. **Siempre viene la clave**, con valor `null` si el evento no la trae.                    |
| `companyName` | String \| null | Nombre de la empresa. Mismo criterio que `companyCode`.                                                        |
| `hash`        | String \| null | Hash de verificación. **Siempre viene la clave**, con valor `null` si no tienes una llave pública configurada. |

<Tip>
  `companyCode`, `companyName` y `hash` **siempre están presentes como clave**, aunque valgan `null`. Puedes leerlos sin verificar existencia, pero sí tienes que contemplar el `null` antes de usar el valor.
</Tip>

## El hash de verificación

Tu endpoint es público, así que cualquiera puede llamarlo. El campo `hash` te deja confirmar que la petición viene de TapiPay y que el payload no fue alterado.

Se genera cifrando con **RSA** contra la llave pública que entregas en el onboarding. Si no configuras una `publicKey`, el campo llega igual pero con valor `null`.

<Warning>
  **No asumas que `hash` viene con valor.** Valida que no sea `null` antes de usarlo. Si tu integración depende de él para autenticar, un `null` inesperado significa que la llave pública no quedó configurada: confírmalo con TapiPay antes de salir a producción.
</Warning>

El hash es un mecanismo entre varios. Los demás (API Key, Bearer token, whitelist de IPs, mTLS) se configuran igual para todos los webhooks y están detallados en [Notificación de pago](/es/webhooks/notificacion-de-pago#validar-que-la-petición-viene-de-tapipay).

## El detalle de error es opcional

Los eventos de tarjeta pueden incluir el código y el mensaje de error que devolvió el proveedor, dentro de un objeto `additionalData`. Que lleguen o no depende de un flag de **tu configuración**, `sendErrorInfo`, que se define en el onboarding.

<Warning>
  **`additionalData` se omite por completo cuando queda vacío.** No llega como objeto vacío ni con campos en `null`: directamente no está la clave. Verifica su existencia antes de leer adentro.
</Warning>

Queda omitido en dos casos: cuando `sendErrorInfo` está apagado, y cuando está prendido pero el proveedor no informó ningún detalle.

```javascript theme={null}
// Correcto: contempla que la clave no exista
const codigo = notificacion.additionalData?.errorCode ?? null;
```

<Note>
  Los eventos de **débito bancario** funcionan distinto: el detalle del rechazo (`bankErrorCode`, `bankErrorDescription`) viaja **siempre**, en la raíz del payload y sin depender de `sendErrorInfo`.
</Note>

## Idempotencia

TapiPay puede reintentar una notificación, así que tu endpoint tiene que tolerar recibir el mismo evento más de una vez. La llave es `operationId`.

<Warning>
  Guarda los `operationId` ya procesados en almacenamiento **persistente** (una tabla con índice único, o Redis), nunca en memoria del proceso: si reinicias el servidor entre el intento original y el reintento, una marca en memoria se pierde y el evento se procesa dos veces.
</Warning>

Los requisitos de tu endpoint (responder 2xx, en menos de 5 segundos, ser alcanzable desde las IPs de TapiPay) son los mismos para todos los webhooks y están en [Notificación de pago](/es/webhooks/notificacion-de-pago#requisitos-de-tu-endpoint).

<Tip>
  Cada intento de envío queda registrado del lado de TapiPay, con su resultado, así que una notificación que creas perdida se puede rastrear.
</Tip>
