# 4 · Desembolso en GTQ

Fase 4 del flujo. Los GTQ ya están en la cuenta de Felix en Nexa; ahora
Tesser instruye a Nexa que los desembolse.

**El desembolso es sincrónico.** Nexa lo ejecuta y devuelve el resultado en
la respuesta de esa misma llamada. No hay ningún estado pendiente que
consultar ni ningún webhook de avance que esperar. Llega un webhook en
exactamente un caso: una falla que aparece en la red de pagos después de una
respuesta `COMPLETED`.

<Callout type="note" title="Alcance de esta integración: un solo beneficiario fijo">

En esta fase Nexa siempre paga al **mismo beneficiario**, configurado una
sola vez fuera de banda y que nunca aparece en el cuerpo de una llamada de
API. Ese beneficiario es el **socio de desembolso**, que ejecuta el
desembolso de última milla hacia los receptores de remesas individuales.

**Nexa no participa en la última milla.** Todos los desembolsos
van al mismo lugar, así que en este contrato no hay datos de beneficiario
por destinatario, ni objetos de destino, ni campos de cumplimiento del
beneficiario. Que Nexa asuma los desembolsos de remesas de última milla es
una etapa posterior del programa y queda fuera de alcance aquí. Véase
[alcance del MVP](/es/flow-nexa/lifecycle#alcance-del-mvp).

</Callout>

<Mermaid
  chart={`sequenceDiagram
    autonumber
    participant T as Tesser (orquestador)
    participant N as Banco Nexa

    Note over T,N: Fase 4 · Desembolso en GTQ al beneficiario fijo
    T->>N: POST /v1/payments (verificación cruzada de gtq_amount) [N]
    N-->>T: 201 con el resultado: pago COMPLETED, o FAILED si se rechaza de forma sincrónica
    opt Falla que aparece después de una respuesta COMPLETED
        N->>T: webhook payment.status.changed (FAILED / RETURNED) [T]
    end`}
/>

## Paso 5 · Crear el pago <N />

**Operación:** [`POST /v1/payments`: Crear pago (op 5)](/api-nexa/payments#create-payment-op-5)
· lo aloja **Nexa**, lo llama **Tesser**.

La solicitud lleva `tesser_quote_id` y `gtq_amount`. El monto es
**redundante a propósito**: Nexa valida que sea igual al monto acreditado en
su propio evento `quote.credit.confirmed` y, si no coincide, devuelve
`422 AMOUNT_MISMATCH`. Un monto explícito dentro de una instrucción que mueve
dinero es un seguro barato contra errores por ID de cotización equivocado.

- **La respuesta es el resultado.** Un `201` significa que el desembolso se
  ejecutó — normalmente `payment_status: "COMPLETED"` y
  `quote_status: "PAYMENT_COMPLETED"`, con `completed_at` y el
  `nexa_payment_id` para la conciliación. Tesser no consulta en bucle ni
  espera ningún evento de confirmación.
- **Condición previa:** la cotización debe estar en `CREDIT_CONFIRMED`. Antes
  de eso se obtiene `409 QUOTE_NOT_CREDITED`; una cotización rechazada da
  `409 QUOTE_REJECTED`.
- **Un solo pago por cotización** en el MVP. Un reintento devuelve `201` con
  el objeto de pago actual; enviar un `gtq_amount` distinto cuando ya existe
  un pago da `409 PAYMENT_CONFLICT`.
- **Un desembolso rechazado también vuelve en esta misma llamada.** Como el
  dinero se mueve dentro de la solicitud, Nexa responde `201` con
  `payment_status: "FAILED"` y `quote_status: "PAYMENT_FAILED"` cuando
  rechaza el desembolso o no puede ejecutarlo durante la llamada. Una falla
  solo es un webhook cuando aparece en la red de pagos *después* de una
  respuesta `COMPLETED`.
- **Una llamada que expira se reintenta, y el reintento se une a la ejecución
  que ya está en curso.** Como el desembolso ocurre dentro de la llamada, un
  tiempo de espera agotado en la red deja a Tesser sin resultado mientras
  Nexa quizá sigue ejecutando. Por eso un reintento que lleva el mismo
  `tesser_quote_id` **se une a esa ejecución en curso y devuelve su resultado
  final**: nunca inicia un segundo desembolso y nunca falla solo porque ya
  haya uno en ejecución. No existe una lectura del *pago* durante la
  ejecución, porque la
  [lectura del pago](/api-nexa/payments#get-payment-optional-op-9) <N /> usa
  como llave `nexa_payment_id`, que solo existe una vez que llega la
  respuesta. La
  [lectura de la cotización](/api-nexa/quotes#get-quote-optional-op-8) <N />
  sí es alcanzable a mitad de la ejecución, pero no es informativa: ningún
  estado intermedio es representable, así que sigue reportando
  `CREDIT_CONFIRMED`. Si el reintento también expira, la recuperación es otro
  reintento o la lectura de la cotización una vez que exista un resultado. Si
  Nexa puede implementar esta unión es la
  [pregunta abierta 5](/es/open-questions).

<div className="what-can-go-wrong">

**Qué puede salir mal: crear el pago**

| `error_code` | HTTP | Cuándo |
| --- | --- | --- |
| `VALIDATION_ERROR` | 400 | Monto mal formado o UUID inválido. |
| `UNAUTHENTICATED` | 401 | Falta el certificado de cliente o la llave de API del cliente, o son inválidos. |
| `QUOTE_NOT_FOUND` | 404 | No existe ninguna cotización con este `tesser_quote_id`. |
| `QUOTE_NOT_CREDITED` | 409 | La acreditación todavía no llega a `CREDIT_CONFIRMED`. |
| `QUOTE_REJECTED` | 409 | La cotización está en `REJECTED`. |
| `PAYMENT_CONFLICT` | 409 | La cotización ya tiene otro pago distinto. |
| `AMOUNT_MISMATCH` | 422 | El `gtq_amount` no es igual al monto acreditado. |
| `INTERNAL_ERROR` | 500 | Falla inesperada del lado de Nexa. |

</div>

## Paso 6 · Falla después de una respuesta `COMPLETED` <T />

**Operación:** [`POST /v1/webhooks/nexa/{organizationId}`: Recibir webhook de Nexa (op 4)](/api-tesser/webhooks#receive-nexa-webhook-op-4)
· lo aloja **Tesser**, lo llama **Nexa**.

Este es el **único** webhook del tramo de desembolso, y Nexa lo envía **solo
cuando algo sale mal después de una respuesta exitosa**: el desembolso se
ejecutó y se reportó como `COMPLETED`, y luego falló o fue devuelto en la red
de pagos. Un rechazo durante la llamada misma nunca se convierte en webhook;
vuelve de forma sincrónica en la creación del pago.

| `event_type` | Significado | `data` |
| --- | --- | --- |
| `payment.status.changed` | El desembolso falló o fue devuelto después del hecho. | `tesser_quote_id`, `nexa_payment_id`, `payment_status` (`FAILED` o `RETURNED`), `quote_status` (`PAYMENT_FAILED` o `PAYMENT_RETURNED`), `status_metadata` (objeto de forma libre: referencias de la red de pagos, motivos de falla) |

**No existe un webhook de éxito.** Una cotización que llega a
`PAYMENT_COMPLETED` y se queda ahí simplemente terminó; el silencio significa
éxito.

`status_metadata` es el único lugar del contrato donde se espera contenido de
forma libre, porque las referencias de las redes de pago y los motivos de
falla sí varían de verdad.

La recuperación desde `PAYMENT_FAILED` y `PAYMENT_RETURNED` es **manual y
fuera de banda** en el POC. No existen semánticas de reintento en la API
([pregunta abierta 7](/es/open-questions)).

Qué red o redes de pago guatemaltecas se usan, si `RETURNED` existe realmente
en esa red, cuánto tiempo después de una respuesta `COMPLETED` puede aparecer
todavía una falla y el contenido de `status_metadata` son la
[pregunta abierta 5](/es/open-questions).

<div className="what-can-go-wrong">

**Qué puede salir mal: notificación de falla**

| Situación | Comportamiento |
| --- | --- |
| `VALIDATION_ERROR` 400 | Envoltorio o `data` mal formados para ese tipo de evento. |
| `UNAUTHENTICATED` 401 | Falta la identidad mTLS, no se reconoce, o no coincide con `{organizationId}`. |
| `INTERNAL_ERROR` 500 | Falla del lado de Tesser. Nexa reintenta. |
| Entrega duplicada | Se deduplica por `event_id`; igual se confirma con 2xx. |
| Un evento de falla para una cotización que ya está en `PAYMENT_FAILED` | Se deduplica o se trata como una operación sin efecto; el estado no se mueve dos veces. |

</div>

## Recuperar el estado

Si se pierde el webhook de falla, el opcional
[`GET /v1/payments/{nexa_payment_id}`](/api-nexa/payments#get-payment-optional-op-9)
<N /> devuelve el `payment_status` y el `status_metadata` actuales, y
[`GET /v1/quotes/{tesser_quote_id}`](/api-nexa/quotes#get-quote-optional-op-8)
<N /> devuelve la cotización consolidada, incluido su subobjeto `payment`.

**Siguiente:** [5 · Tesorería](/es/flow-nexa/treasury).
