# Guía de webhooks

Los webhooks transportan todas las decisiones asincrónicas de esta
integración. Hay dos flujos, y esta página cubre los dos.

| Flujo | Receptor | Operación |
| --- | --- | --- |
| **De Nexa a Tesser**, decisiones de acreditación y fallas del desembolso | Tesser <T /> | [`POST /v1/webhooks/nexa/{organizationId}`](/api-tesser/webhooks#receive-nexa-webhook-op-4) |
| **De Tesser a Nexa**, estado del retiro para tesorería | Nexa <N /> | [`POST /v1/webhooks/tesser`](/api-nexa/webhooks#receive-tesser-webhook-nexa-implements-this) |

## Nexa → Tesser

### El envoltorio

Todo evento de Nexa hacia Tesser usa el mismo sobre:

```json
{
  "event_id": "f31c9a4e-6b2d-4a91-9f0e-2c3b7d5a1e44",
  "event_type": "quote.credit.confirmed",
  "occurred_at": "2026-10-14T15:30:02Z",
  "data": { }
}
```

- `event_id` es un UUID generado por Nexa. **Es la clave de deduplicación.**
- `occurred_at` está en formato RFC 3339 UTC.
- `data` varía según el `event_type`, como se detalla abajo.

### Catálogo de eventos

| `event_type` | Significado | `data` |
| --- | --- | --- |
| `quote.credit.sent` | Nexa decidió acreditar; la instrucción ya se envió al core bancario | `tesser_quote_id`, `quote_status: "CREDIT_SENT"`, `gtq_amount` |
| `quote.credit.confirmed` | Los GTQ ya están disponibles en la cuenta de Felix en Nexa | `tesser_quote_id`, `quote_status: "CREDIT_CONFIRMED"`, `gtq_amount` (valor autoritativo, que puede diferir de `quoted_gtq` según lo decida Nexa), `credited_at` |
| `quote.additional_funds_requested` | Nexa quiere más USDC antes de acreditar | `tesser_quote_id`, `quote_status: "DEPOSIT_REPORTED"`, `usdc_total_reported`, `usdc_additional_requested`, `note` (texto libre) |
| `quote.rejected` | Nexa se niega a acreditar | `tesser_quote_id`, `quote_status: "REJECTED"`, `reason_code` (`RISK`, `AMOUNT` u `OTHER`), `note` |
| `payment.status.changed` | El desembolso **falló o fue devuelto** después de que Nexa ya había respondido `COMPLETED`. Se envía solo ante una falla; no existe un evento de éxito | `tesser_quote_id`, `nexa_payment_id`, `payment_status` (`FAILED` o `RETURNED`), `quote_status`, `status_metadata` (objeto de forma libre: referencias de la red de pagos, motivos de la falla) |

El desembolso en sí es **sincrónico**: `POST /v1/payments` devuelve el
resultado, así que un desembolso exitoso no produce ningún evento. Véase
[4 · Desembolso en GTQ](/es/flow-nexa/gtq-payout).

`status_metadata` aparece únicamente en `payment.status.changed`, que es
donde las referencias de la red de pagos y los motivos de la falla sí varían
de verdad.

### Contrato de entrega

**Al menos una vez.** Tesser deduplica por `event_id`. Nexa reintenta ante
cualquier respuesta que no sea 2xx, con backoff exponencial, propuesto hasta
por 24 horas. La política real del emisor de Nexa es la
[pregunta abierta 6](/es/open-questions).

**Éxito = cualquier 2xx con cuerpo vacío.** Una respuesta que no sea 2xx, o
un timeout, significa reintento.

**El orden no está garantizado. Aplica la regla de alcanzabilidad, es decir,
solo avance de estado.** La [máquina de estados](/es/flow-nexa/lifecycle)
define qué estados son alcanzables desde cuáles. Para el aspecto de *avance
de estado* de cada evento, el receptor compara el `quote_status` del evento
contra el estado actual:

| Relación del `quote_status` del evento con el estado actual | Comportamiento del receptor |
| --- | --- |
| **Alcanzable hacia adelante** desde el estado actual | Se aplica. Es un avance genuino. |
| El **estado actual mismo**, o un **estado anterior** | Se responde `200` y no se cambia nada. Es un duplicado viejo o un reordenamiento. |
| Una **rama distinta o un estado terminal en conflicto**, por ejemplo `quote.credit.sent` después de `quote.rejected`, o `PAYMENT_FAILED` después de `REJECTED` | Se responde `200` **y se levanta una alerta de operaciones.** Esto es un incidente de integridad de datos entre las dos empresas, no un reordenamiento. Ignorarlo en silencio ocultaría una divergencia real de dinero. |

**El evento de falla tardía no es un conflicto.** `PAYMENT_FAILED` y
`PAYMENT_RETURNED` son *alcanzables hacia adelante* desde
`PAYMENT_COMPLETED`, así que una falla que llega después de una respuesta de
desembolso exitosa se maneja con la cláusula normal de alcanzabilidad hacia
adelante: se aplica, sin alerta. Es la vía esperada de la falla tardía, y es
el único evento de desembolso que existe.

**Los eventos que no avanzan el estado también se procesan.** Algunos eventos
traen una acción o información nueva sin mover la máquina de estados:
`quote.additional_funds_requested` (la cotización se queda en
`DEPOSIT_REPORTED`, pero Tesser debe actuar) y cualquier
`payment.status.changed` que repita una falla ya registrada. Estos se
procesan en la **primera entrega de cada `event_id`**; las entregas
posteriores del mismo `event_id` se deduplican como siempre. La regla de
alcanzabilidad rige únicamente el campo `quote_status`, nunca si el contenido
del evento se procesa o no.

### Autenticación

Nexa presenta su certificado de cliente; Tesser resuelve la identidad de Nexa
a partir de la huella digital registrada, y la identidad resuelta debe
coincidir con el `{organizationId}` de la ruta. Una discrepancia da `401`.
El detalle completo está en [Autenticación](/es/authentication).

La conectividad y las credenciales se pueden comprobar sin enviar ningún
evento:
[`HEAD /v1/webhooks/nexa/{organizationId}`](/api-tesser/webhooks#verify-webhook-connectivity-op-4)
<T />.

## Tesser → Nexa

Nexa aloja
[`POST /v1/webhooks/tesser`](/api-nexa/webhooks#receive-tesser-webhook-nexa-implements-this)
<N /> para recibir los **webhooks salientes estándar** de Tesser. En este POC
la suscripción que Nexa necesita es la de **actualizaciones del estado del
retiro**, que da soporte a
[`POST /v1/treasury/withdrawals`](/api-tesser/treasury#create-withdrawal)
<T /> en la [fase 5](/es/flow-nexa/treasury).

El sobre y los tipos de evento son los que Tesser ya tiene. Este portal
documenta *cuál* suscripción necesita Nexa y enlaza a
<a href="https://docs.tesser.xyz" target="_blank">docs.tesser.xyz</a> para las
cadenas de los tipos de evento, en lugar de volver a especificar un contrato
que ya existe y ya está en producción en otro lado.

Autenticación de la entrega: hoy, la firma Ed25519 del encabezado
`X-Tesser-Signature`, más un certificado de cliente de Tesser en cuanto esté
listo el trabajo de mTLS saliente del emisor. Ver
[Autenticación](/es/authentication).

## Lista de verificación para quien implementa (ambas direcciones)

1. **Deduplicar por el id del evento** antes de hacer cualquier otra cosa.
   "Al menos una vez" significa que los duplicados son tráfico normal, no un
   incidente.
2. **Responder rápido y procesar después.** Cualquier 2xx con cuerpo vacío da
   por terminado el intento de entrega. No conviene mantener la conexión
   abierta mientras se hace el trabajo posterior.
3. **Devolver 5xx solo cuando un reintento pueda servir de algo.** Un payload
   mal formado es `400` y nunca va a tener éxito; reintentarlo desperdicia el
   tiempo de ambas partes.
4. **Nunca deducir el estado a partir del orden de llegada.** Hay que
   comparar `quote_status` contra el estado actual y aplicar la regla de
   alcanzabilidad.
5. **Alertar sobre los eventos en conflicto en lugar de descartarlos.** Un
   estado terminal en conflicto significa que las dos empresas no coinciden
   respecto al dinero.
6. **Mantener una vía de recuperación.** Si se pierde un evento,
   [`GET /v1/quotes/{tesser_quote_id}`](/api-nexa/quotes#get-quote-optional-op-8)
   <N /> y
   [`GET /v1/payments/{nexa_payment_id}`](/api-nexa/payments#get-payment-optional-op-9)
   <N /> responden "¿en qué estado creen ustedes que está?" en una sola
   llamada.
