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

# Checkout in plain HTML

> A complete funnel — cart, checkout, 3DS, one-click upsells — in three HTML files. No React, no bundler, no build step.

# Checkout in plain HTML

**Time**: \~15 minutes | **Difficulty**: Beginner | **Type**: part of [Examples](/developer-tools/examples/introduction)

<Info>
  **Finished project:** [`headless-vanilla`](https://github.com/TagadaPay/examples/tree/main/headless-vanilla). Clone it if you want the working funnel first — or keep reading and rebuild the three pages yourself.
</Info>

Proof that the Headless SDK needs **zero tooling**: three static HTML files, the SDK from a CDN, and you get a real store — catalog, cart, card payment with automatic 3DS, and one-click post-purchase upsells. If you can host a folder, you can host this.

| Page        | File             | SDK calls                                                                        |
| ----------- | ---------------- | -------------------------------------------------------------------------------- |
| Home / cart | `index.html`     | `catalog.listProducts()` → `checkout.createSessionUrl()`                         |
| Checkout    | `checkout.html`  | `parseTokensFromUrl()` → `loadSession()` → `tokenizeCard()` → `processPayment()` |
| Thank you   | `thank-you.html` | `offers.listOffers()` → `offers.processOfferPayment()`                           |

***

## Setup — two lines to edit

No `.env`, no build. Provision a demo store from the terminal, then paste your store id:

```bash theme={null}
npx -p @tagadapay/node-sdk tagada-init you@example.com
```

```js theme={null}
// assets/config.js
export const STORE_ID = 'store_xxxxxx';        // ← from tagada-init
export const ENVIRONMENT = 'production';       // or 'development'
```

Serve the folder with anything (`npx serve`, nginx, S3, a USB stick) and open `index.html`.

<Note>
  The SDK ships as an IIFE bundle on jsDelivr — `window.TagadaHeadless` appears after one `<script>` tag. `TagadaHeadless.create({ storeId, environment })` gives you the same client as the npm package.
</Note>

***

## Page 1 — cart to checkout in one call

The home page lists real products from your catalog and keeps the cart in `localStorage`. Checkout is a single call — `createSessionUrl` creates the session **and** builds the URL to send the customer to:

```js theme={null}
const { url } = await tagada.checkout.createSessionUrl({
  items,                        // [{ variantId, quantity }]
  currency: state.currency,
  checkoutPath: '/checkout.html',
});
window.location.href = url;
```

## Page 2 — the checkout

`checkout.html` starts with the two lines that make redirects painless. If the customer is coming back from a 3DS challenge or a processor redirect, `maybeResumeFromUrl()` polls the payment to a terminal state and hands you the result; otherwise it returns `null` and the page renders normally:

```js theme={null}
const resumed = await tagada.payment.maybeResumeFromUrl();
if (resumed?.status === 'succeeded') {
  window.location.replace(`./thank-you.html?orderId=${resumed.order.id}`);
}
```

Then the normal path — read the tokens, load the session, and on submit: save the customer, pick a shipping rate, tokenize, charge:

```js theme={null}
const tokens = CheckoutModule.parseTokensFromUrl();
const session = await tagada.checkout.loadSession(tokens.checkoutToken, tokens.sessionToken);

// on submit
await tagada.checkout.updateCustomerAndAddress(session.id, { customer, shippingAddress });
const rates = await tagada.checkout.getShippingRates(session.id);
if (rates.length) await tagada.checkout.selectShippingRate(session.id, rates[0].id);

const { tagadaToken } = await tagada.payment.tokenizeCard({ cardNumber, expiryDate, cvc, cardholderName });
const result = await tagada.payment.processPayment({
  checkoutSessionId: session.id,
  tagadaToken,
  paymentMethod: 'credit-card',
});
```

<Tip>
  `tokenizeCard()` talks to Basis Theory directly from the browser — raw card data never touches your server, which keeps you out of PCI scope. If `processPayment` returns `requires_redirect`, navigate to `result.redirectUrl` (the example shows the POST-form variant too) and `maybeResumeFromUrl()` finishes the job when the customer comes back.
</Tip>

## Page 3 — one-click upsells

The thank-you page lists upsell offers and charges the **saved card** in one click — a MIT charge, no card form the second time:

```js theme={null}
const offers = await tagada.offers.listOffers({ type: 'upsell' });

// when the customer clicks "Add to my order"
await tagada.offers.previewOffer({ offerId });          // re-validate pricing
const result = await tagada.offers.processOfferPayment({
  offerId,
  mainOrderId: orderId,
});
```

Same funnel shape as the [funnel demos](/developer-tools/funnel-demos/introduction) — this is just the smallest possible implementation of it.

***

## Next

<CardGroup cols={2}>
  <Card title="Finished project on GitHub" icon="github" href="https://github.com/TagadaPay/examples/tree/main/headless-vanilla">
    Three HTML files, ready to serve — edit `config.js` and open
  </Card>

  <Card title="Same thing in React" icon="react" href="/developer-tools/headless-sdk/build-store-with-ai">
    `headless-react-store` — the fork-ready boutique storefront
  </Card>

  <Card title="Checkout flow reference" icon="cart-shopping" href="/developer-tools/headless-sdk/checkout-flow">
    Sessions, addresses, shipping rates, promo codes
  </Card>

  <Card title="Offers reference" icon="gift" href="/developer-tools/headless-sdk/offers">
    `listOffers`, `previewOffer`, `processOfferPayment`
  </Card>
</CardGroup>
