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

# Checkout web (SDK)

> Crear el payment intent en el servidor y montar el widget en el navegador con @zopay/js.

El SDK del navegador **no crea payment intents** y **nunca habla con ZoPay directamente**. Tu servidor crea el intent contra Connect API; el navegador monta el widget con el envelope recibido, y el widget polea estado vía un callback que tú implementas (`checkStatus`) que proxea la lectura a tu backend.

## Flujo en tres pasos

1. **Servidor** llama a `POST /connect/v1/payment-intents` con la clave Connect.
2. **Servidor** devuelve el envelope `PaymentIntent` al frontend (nunca la clave).
3. **Frontend** monta el widget con `mountPaymentWidget(target, { intent, checkStatus, … })`.

## 1. Crear el intent (servidor)

```bash theme={null}
curl -X POST "https://api.zopay.cash/connect/v1/payment-intents" \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "25.00",
    "currencies": ["USDT"],
    "external_ref": "user_alice"
  }'
```

Devuelve el envelope completo `PaymentIntent` (`{ id, amount, currency, addresses, … }`). Referencia: [Crear Payment Intent](/api-reference/endpoints/post-payment-intents).

## 2. Devolver el envelope al cliente

Tu propio endpoint debe devolver el envelope al navegador. **No** expongas `sk_live_…` al navegador.

```http theme={null}
POST /api/zopay/checkout/session  →  { id, amount, currency, addresses, … }
```

## 3. Endpoint de lectura de estado (servidor)

El widget no lee estado directo de ZoPay — proxea por tu backend:

```ts theme={null}
// GET /api/zopay/status/:id
app.get('/api/zopay/status/:id', async (req, res) => {
  const r = await fetch(
    `https://api.zopay.cash/connect/v1/payment-intents/${req.params.id}`,
    { headers: { Authorization: `Bearer ${process.env.ZOPAY_SECRET}` } }
  );
  res.json(await r.json());
});
```

## 4. Montar el widget

```bash theme={null}
npm install @zopay/js
```

```html theme={null}
<div id="pay"></div>
```

```ts theme={null}
import { mountPaymentWidget, normalizeIntent } from "@zopay/js";
import "@zopay/js/styles.css";

const intent = await fetch("/api/zopay/checkout/session", { method: "POST" })
  .then(r => r.json());

const handle = mountPaymentWidget(document.getElementById("pay")!, {
  intent,
  checkStatus: async () => {
    const res = await fetch(`/api/zopay/status/${intent.id}`);
    return normalizeIntent(await res.json());
  },
  onSuccess: (info) => console.log("paid", info.txHash, info.amountReceived),
  onError:   (info) => console.error("failed", info),
  onExpire:  ()     => console.warn("timed out"),
  theme: "light",
});

// React/Vue: limpia en unmount.
// useEffect(() => () => handle.destroy(), []);
```

## Probar en sandbox

Con una clave `sk_test_…`, el intent se crea con `livemode: false`. Para simular el cobro sin esperar un depósito on-chain real, llama desde tu backend a [`POST /test/payment-intents/{id}/simulate-deposit`](/api-reference/endpoints/post-test-payment-intents-by-id-simulate-deposit) — el widget detectará el cambio de estado en el siguiente poll y disparará `onSuccess`.

## Siguiente paso

* [mountPaymentWidget](/sdk-reference/mount-payment-widget) — todas las opciones del widget.
* [createPoller](/sdk-reference/create-poller) — poller sin UI para integraciones custom.
* [normalizeIntent](/sdk-reference/normalize-intent) — mapeo entre la API y `StatusInfo`.
* [Lo que NO debes hacer](/sdk-reference/index#lo-que-no-debes-hacer) — el modelo de seguridad del SDK.
