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

# Payment Intents

> Envelope, ciclo de vida, pagos parciales y polling de los payment intents.

Un **payment intent** representa una petición de cobro concreta: un monto, los activos que el pagador puede usar, metadata opcional, y las direcciones a las que enviar fondos. Creado vía [`POST /payment-intents`](/api-reference/endpoints/post-payment-intents), leído vía [`GET /payment-intents/{id}`](/api-reference/endpoints/get-payment-intents-by-id), listado vía [`GET /payment-intents`](/api-reference/endpoints/get-payment-intents).

El **mismo envelope** lo devuelven los tres endpoints + el simulador sandbox + cualquier evento del log relacionado al intent. Construye tu cliente alrededor de este envelope y tienes un solo decoder para cada entry point.

## El envelope

Todo campo abajo está **siempre presente** en la respuesta, con `null` cuando no aplica. Es deliberado — código del partner que hace `response.metadata` sin guard no se rompe en intents sin metadata. Estilo Stripe.

```jsonc theme={null}
{
  "id":            "ef0cb276-399a-4c21-b33b-ea63ddc48246",   // UUID string
  "object":        "payment_intent",                          // literal fijo
  "amount":        5.0,                                       // monto pedido
  "currency":      "USDT",                                    // asset principal, MAYÚSCULAS
  "status":        "pending",                                 // pending | completed | expired | failed
  "ref_code":      "EDEA9",                                   // id de 5 chars para hosted page
  "external_ref":  "user_42",                                 // tu clave de atribución (o null)
  "description":   null,                                      // proporcionado por el partner (o null)
  "metadata":      null,                                      // objeto opcional del partner (o null)
  "addresses": [                                              // destinos por (asset, red)
    {
      "asset":   "USDT",
      "network": "sol",                                       // código lowercase (o null en race del catálogo)
      "address": "zSAND9FBqeoVb9mDr2tBKWiBtPGZfq8f"           // wallet on-chain, verbatim
    }
  ],
  "created_at":    "2026-06-09T19:40:13.042621Z",
  "expires_at":    "2026-06-10T19:40:13.195347Z",
  "completed_at":  null,                                      // se setea al pasar a completed

  "deposit":       null,                                      // detalles del ÚLTIMO crédito on-chain
                                                              // (ver "El objeto deposit" abajo)

  "amount_received":   0.0,                                   // suma acumulada de depósitos completados
  "amount_remaining":  5.0,                                   // max(0, amount - amount_received)
  "partial_payment":   false                                  // booleano canónico "está incompleto"
}
```

## Transiciones de estado

```
                    POST /payment-intents
                              │
                              ▼
                       ┌──────────┐
                       │ pending  │  ◄──── intents arrancan aquí
                       └────┬─────┘
                            │
        ┌───────────────────┼────────────────────┐
        │                   │                    │
        │ (acumulado >=     │ (expires_at        │ (depósito parcial
        │  amount, dentro   │  alcanzado sin     │  llega; el intent
        │  de 0.01 de       │  pago completo)    │  se queda aquí)
        │  tolerancia)      │                    │
        ▼                   ▼                    │
   ┌──────────┐       ┌──────────┐               │
   │completed │       │ expired  │               │
   └──────────┘       └──────────┘               │
                                                 │
                       (sin transición) ◄────────┘
```

`failed` está reservado para uso futuro; ningún path actual lo emite. Construye tu cliente para ignorar estados futuros desconocidos sin romper.

## Campos cumulativos

Los tres campos cumulativos cierran el gap del pago parcial: un depósito de $3 contra un intent de $5 es estructuralmente distinto de "todavía no llegó nada".

| Campo              | Qué es                                                                                  | Cuándo usarlo                                     |
| ------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `amount_received`  | Suma de depósitos completados contra este intent. Arranca en `0.0`.                     | Mostrar "recibido hasta ahora" en la UI parcial.  |
| `amount_remaining` | `max(0, amount - amount_received)`. Sin tolerancia aplicada. Clampea a 0 en sobre-pago. | Mostrar "envía \$X más para completar".           |
| `partial_payment`  | `true` sólo si `amount_received > 0 AND status == 'pending'`.                           | El booleano canónico para "renderiza UI parcial". |

**Usa `partial_payment` como el boolean de dispatch** — respeta transitivamente la tolerancia de 0.01 que usa el backend para decidir completion. Un intent dentro de tolerancia ya pasó a `completed`, así que `partial_payment` es `false` y el booleano te dice renderiza éxito, no parcial.

## Ciclo de pago parcial

Ejemplo concreto: intent de $5, pagador envía $3 y luego \$2.

```
T=0      POST /payment-intents → minteado
         status=pending  amount_received=0.0  partial_payment=false  amount_remaining=5.0

T=10s    Depósito de $3 aterriza on-chain
         status=pending  amount_received=3.0  partial_payment=true   amount_remaining=2.0
         deposit = { amount_received: 3.0, ... }    ◄── detalles del último crédito

T=60s    Depósito de $2 aterriza on-chain
         status=completed completed_at=<T+60s>  partial_payment=false  amount_remaining=0.0
         amount_received=5.0
         deposit = { amount_received: 2.0, ... }    ◄── ÚLTIMO, no acumulado

         (webhook payment.completed dispara aquí)
```

Dos cosas para anotar:

1. **`deposit` es el ÚLTIMO crédito, no el acumulado.** En el flujo parcial→completo, `deposit.amount_received` muestra los $2 (el último). El `amount_received` top-level muestra el acumulado $5. **No sumes** `deposit.amount_received` entre polls — usa el campo top-level.
2. **La tolerancia es invisible para ti.** Un depósito de $4.995 contra un intent de $5.00 pasa el `status` a `completed` (dentro de la tolerancia de 0.01 que usa el flujo de crédito). `partial_payment` es `false`. Confía en el booleano; no reimplementes el threshold del lado del cliente.

## Polling

Un widget poleando `GET /payment-intents/{id}` para estado debe despachar en un árbol de decisión pequeño:

```ts theme={null}
async function poll(intentId: string) {
  const intent = await fetch(`/api/zopay/status/${intentId}`).then(r => r.json());

  if (intent.status === "completed")  return onSuccess(intent.deposit);
  if (intent.status === "expired")    return intent.amount_received > 0
                                          ? onExpiredWithCredits(intent.amount_received)
                                          : onExpired();
  if (intent.partial_payment)         return showPartialUI(
                                          intent.amount_received,
                                          intent.amount_remaining
                                        );

  // status === "pending", amount_received === 0 → intent fresco, esperando primer crédito.
  return showWaitingUI();
}
```

El SDK del navegador hace esto por ti — ver [`mountPaymentWidget`](/sdk-reference/mount-payment-widget) y [`normalizeIntent`](/sdk-reference/normalize-intent).

## El objeto `deposit`

`deposit` se puebla cuando un crédito aterriza. En el flujo parcial→completo refleja el **ÚLTIMO** crédito en cada lectura (no el acumulado). Usa `amount_received` (top-level) para acumulado.

```jsonc theme={null}
{
  "deposit": {
    "amount_received":   5.0,                                  // amount de ESTE crédito, no acumulado
    "currency":          "USDT",
    "network":           "sol",
    "address_receiver":  "zSAND9FBqeoVb9mDr2tBKWiBtPGZfq8f",
    "address_sender":    "zSAND_simulator_sender",             // "..._simulator_sender" en sandbox
    "tx_hash":           "5cc7a1fa..."                         // hash on-chain (determinístico en sandbox)
  }
}
```

Este objeto es **idéntico campo-por-campo** al shape `data` del evento webhook `payment_intent.completed`. Partners que hacen reconciliación pull (GET) y push (webhook) pueden usar un solo decoder para los dos.

## Transacciones atribuidas a Connect

Cada fila que devuelve `GET /transactions` carga un campo `payment_intent_id`. Si es no-`null`, esa transacción fue un crédito contra el intent nombrado (flujo SDK-driven). Si es `null`, la transacción vino por otro path (depósito directo a dirección org-owned, transferencia, funding interno). Úsalo para deep-linkear filas de transacción a su intent originador en los dashboards del partner.

## Ver también

* [Crear Payment Intent](/api-reference/endpoints/post-payment-intents)
* [Consultar Payment Intent](/api-reference/endpoints/get-payment-intents-by-id)
* [Direct vs. whitelabel flows](/api-reference/direct-vs-whitelabel) — `external_ref` decide a qué tabla de balance cae el crédito.
* [Eventos](/api-reference/events) — `payment_intent.completed` y `payment_intent.payment_received`.
