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

# Notificación de pago

> El webhook con el que TapiPay te avisa que un pago se confirmó, falló o se revirtió.

Este endpoint **lo implementas tú**, en tu backend. TapiPay lo llama cada vez que un pago cambia de estado, para que actualices la deuda en tu sistema sin tener que consultar la API en loop.

<Warning>
  Es la **única** fuente confiable del estado de un pago. El evento `paymentSubmitted` del [SDK](/es/portal-de-pagos/sdk-medios-de-pago#eventos) indica que el usuario envió el formulario, no que el pago se acreditó. Marca una deuda como pagada solo cuando llega esta notificación con `status: "confirmed"`.
</Warning>

<Note>
  Esta notificación **no la emite la Facade API**, que no implementa webhooks. La envía la plataforma de **pago referenciado**, y se configura **a nivel de plataforma durante el onboarding** con el equipo de TapiPay: no hay endpoint para dar de alta ni cambiar la URL. Las peticiones salen de las IPs de esa plataforma, no del host de la Facade API, y te las entregan en el onboarding.
</Note>

## Configuración

Tú defines la **URL completa** (host y path) a la que TapiPay va a enviar las notificaciones. Entregas dos:

| Ambiente     | Para qué                |
| ------------ | ----------------------- |
| Homologación | Pruebas de integración. |
| Producción   | Transacciones reales.   |

TapiPay no impone una ruta. Cualquier path accesible sirve: `/api/webhooks/confirm-payment`, `/tapi/callback`, `/notifications`, `/payment/status`.

### Requisitos de tu endpoint

<ResponseField name="Accesible" required>
  Alcanzable desde las IPs de origen de TapiPay que te entregan en el onboarding.
</ResponseField>

<ResponseField name="Respuesta 2xx" required>
  Devuelve cualquier código HTTP 2xx para confirmar la recepción. **TapiPay no lee el body de tu respuesta**, solo el código de estado, así que puedes responder vacío.
</ResponseField>

<ResponseField name="Menos de 5 segundos" required>
  Si tardas más, la petición se considera fallida y entra en reintentos. Si necesitas procesamiento pesado, encola y responde antes.
</ResponseField>

## El payload

`POST` con `Content-Type: application/json` al endpoint que definiste.

| Campo               | Tipo   | Presente | Descripción                                                                                                                                                                             |
| ------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `operationId`       | String | Siempre  | ID de la operación en TapiPay. **Úsalo como llave de idempotencia.**                                                                                                                    |
| `status`            | String | Siempre  | `confirmed` (exitoso), `failed` (error) o `reversed` (reversión posterior).                                                                                                             |
| `amount`            | Number | Siempre  | Monto cobrado.                                                                                                                                                                          |
| `externalPaymentId` | String | Siempre  | Tu ID de la operación de pago, para trazabilidad.                                                                                                                                       |
| `externalClientId`  | String | Siempre  | Tu identificador del usuario final.                                                                                                                                                     |
| `externalRequestId` | String | Siempre  | Tu identificador de la deuda asociada. Es el campo con el que correlacionas la notificación contra tu sistema.                                                                          |
| `clientUsername`    | String | Siempre  | Username de tu cliente en TapiPay.                                                                                                                                                      |
| `companyCode`       | String | Siempre  | Código de la compañía.                                                                                                                                                                  |
| `companyName`       | String | Siempre  | Nombre de la compañía.                                                                                                                                                                  |
| `paymentMethod`     | String | Siempre  | Medio con el que se pagó (por ejemplo, `TRANSFER` o `CARD`). Ver [medios de pago](/es/conceptos/medios-de-pago).                                                                        |
| `type`              | String | Siempre  | Tipo de notificación.                                                                                                                                                                   |
| `createdAt`         | String | Siempre  | Creación de la operación, en ISO 8601.                                                                                                                                                  |
| `updatedAt`         | String | Siempre  | Última actualización de la operación, en ISO 8601.                                                                                                                                      |
| `additionalData`    | Object | Siempre  | Objeto dinámico con los campos opcionales que se cargaron al crear la deuda. Ver abajo.                                                                                                 |
| `hash`              | String | Variable | Hash de seguridad, ligado al mecanismo de hash cifrado. No lo asumas presente: valida que venga con valor antes de usarlo y confirma con TapiPay si tu integración lo tiene habilitado. |

### `additionalData` es dinámico

<Warning>
  `additionalData` reenvía **exactamente** los campos opcionales que se cargaron al crear la deuda, así que sus claves varían entre clientes y entre servicios. **No hay un conjunto de campos garantizado.** Valida la existencia de cada campo antes de usarlo: no asumas que alguno va a estar presente.
</Warning>

### Ejemplo

Los campos dentro de `additionalData` son **ilustrativos**: muestran lo que un cliente podría enviar, no un contrato.

```json theme={null}
{
  "operationId": "1620990a-d124-4e1a-8dcd-7d8402824f37",
  "clientUsername": "miempresa.prod.mx",
  "status": "confirmed",
  "externalPaymentId": "8c06bb4a-f1a6-4dcf-859a-c71399e6d642",
  "externalClientId": "CLI-00042",
  "externalRequestId": "INV-2026-001",
  "createdAt": "2026-06-21T23:41:55.661Z",
  "updatedAt": "2026-06-21T23:41:55.661Z",
  "companyCode": "MX-S-12345",
  "companyName": "ACME",
  "amount": 350.00,
  "hash": null,
  "type": "SERVICE",
  "paymentMethod": "TRANSFER",
  "additionalData": {
    "referenceCode": "00001234567890",
    "amountType": "OPEN",
    "amount": 350.00,
    "description": "Pago de servicio",
    "allowPartialPayments": true,
    "recurringDebt": false,
    "overduePayment": true,
    "debtReference": "00001234567890",
    "debtExpirationDate": "2026-06-30"
  }
}
```

## Validar que la petición viene de TapiPay

Tu endpoint es público, así que cualquiera puede llamarlo. TapiPay ofrece **cinco mecanismos** que se configuran en el onboarding. Puedes activar uno o combinar varios.

| Mecanismo                    | Cómo funciona                                                               | Qué validas                                                           |
| ---------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **API Key**                  | TapiPay envía un header `x-api-key` con una clave secreta.                  | Presencia y valor del header.                                         |
| **Bearer token (JWT)**       | TapiPay envía un JWT en `Authorization: Bearer ...`.                        | La firma del token.                                                   |
| **Whitelist de IPs**         | Las peticiones salen de las IPs de origen que te entregan.                  | Que la IP de origen esté en tu lista.                                 |
| **Certificado mTLS (X.509)** | TLS mutuo: TapiPay presenta un certificado de cliente.                      | El certificado contra tu CA.                                          |
| **Hash cifrado**             | TapiPay incluye un `hash` en el payload, generado con una llave compartida. | Que el hash coincida, lo que confirma que el payload no fue alterado. |

<Tip>
  Combina al menos dos, por ejemplo API Key más whitelist de IPs, o Bearer token más mTLS. Un solo mecanismo basado en header es vulnerable si la clave se filtra.
</Tip>

## Reintentos e idempotencia

<Warning>
  **TapiPay reintenta si tu endpoint falla.** Eso significa que puedes recibir la misma notificación más de una vez. Implementa idempotencia por `operationId` o vas a procesar el mismo pago dos veces.
</Warning>

El orden correcto en tu handler es: validar el origen, responder 2xx rápido, y recién después procesar.

```javascript theme={null}
import express from "express";

const app = express();

app.post("/tapi/notificaciones", express.json(), async (req, res) => {
  // 1. Valida el origen antes de mirar el body
  if (req.header("x-api-key") !== process.env.TAPI_WEBHOOK_KEY) {
    return res.sendStatus(401);
  }

  const { operationId, status, amount, externalRequestId } = req.body;

  // 2. Responde antes de cualquier trabajo pesado: el límite son 5 segundos
  res.sendStatus(200);

  // 3. Idempotencia por operationId
  if (await yaProcesada(operationId)) return;
  await marcarProcesada(operationId);

  // 4. Ahora sí, el procesamiento real
  await encolar({ operationId, status, amount, externalRequestId });
});
```

<Note>
  `yaProcesada` y `marcarProcesada` tienen que apoyarse en almacenamiento **persistente** (una tabla con `operationId` único, o Redis), no en memoria del proceso: si reinicias el servidor entre el intento original y el reintento, una marca en memoria se pierde y el pago se procesa de nuevo.
</Note>

## Cómo probarlo

TapiPay no puede alcanzar tu `localhost`, así que necesitas una URL pública. Dos formas, según qué quieras verificar.

<Tabs>
  <Tab title="Inspeccionar el payload">
    Para ver el payload real sin escribir código, usa un receptor público como [webhook.site](https://webhook.site) y entrega esa URL como tu endpoint de homologación. Cada notificación queda registrada con sus headers y su body.

    Sirve para confirmar qué llega en `additionalData` en tu caso concreto, que es la parte que no se puede documentar de antemano.

    <Warning>
      Solo para homologación con datos de prueba. Un receptor público expone el payload a cualquiera que tenga la URL: nunca apuntes producción ahí.
    </Warning>
  </Tab>

  <Tab title="Probar tu código">
    Para ejercitar tu handler real, expón tu servidor local con un túnel:

    ```bash theme={null}
    # con cloudflared
    cloudflared tunnel --url http://localhost:3000

    # o con ngrok
    ngrok http 3000
    ```

    Entrega la URL pública del túnel (más tu path) como endpoint de homologación. Ojo: la URL cambia en cada reinicio del túnel salvo que uses un dominio fijo.
  </Tab>
</Tabs>

Mientras esperas que TapiPay dispare una notificación real, puedes ejercitar tu handler con el payload de ejemplo de esta página:

```bash theme={null}
curl --request POST \
  --url 'http://localhost:3000/tapi/notificaciones' \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: TU_WEBHOOK_KEY' \
  --data '{
    "operationId": "1620990a-d124-4e1a-8dcd-7d8402824f37",
    "status": "confirmed",
    "amount": 350.00,
    "externalRequestId": "INV-2026-001",
    "externalClientId": "CLI-00042",
    "paymentMethod": "TRANSFER",
    "additionalData": {}
  }'
```

Llámalo **dos veces con el mismo `operationId`**: es la forma más rápida de comprobar que tu idempotencia funciona antes de salir a producción.
