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

# Embedded Checkout

> Embed Alguna's checkout directly in your product with the Checkout SDK

Embedded Checkout renders Alguna's checkout inside your own pages — a pricing page, an in-app upgrade flow, or any purchase surface — while payment collection stays on Alguna's secure origin. Your server mints a checkout session with the API; the browser mounts it with the `@alguna/checkout-sdk` package.

***

## How it works

1. **Your server** calls `POST /beta/checkout-sessions` with your secret API key and receives a session containing a browser-safe `session_token` and a hosted `url`.
2. **Your page** passes that `url` to the Checkout SDK, which mounts the checkout in an iframe and keeps its size, theme, and lifecycle in sync with your page.
3. **The customer pays** inside the embedded component. Card details never touch your site or ours — they go directly to the payment processor.
4. **Alguna fulfills the purchase** server-side and notifies you via the `checkoutsession.completed` webhook (or you poll the session).

The secret API key must never reach the browser. Only the session `url` and `session_token` are browser-safe.

***

## Creating a session

```bash theme={null}
curl https://api.alguna.io/beta/checkout-sessions \
  -H "Authorization: Bearer $ALGUNA_API_KEY" \
  -H "Alguna-Version: 2026-04-01" \
  -H "Content-Type: application/json" \
  -d '{
    "plan_id": "pln_033ZYEaFZURxhK5a1cUAgQ",
    "client_reference_id": "order_1234",
    "expires_in_seconds": 3600
  }'
```

The response includes `id`, `session_token`, `url`, `amount`, `currency`, and `expires_at`. Sessions expire after `expires_in_seconds` (default 24 hours, minimum 5 minutes, maximum 7 days); an expired session can no longer be rendered or paid.

### What the session charges

Provide exactly one of:

| Field        | Behavior                                                                      | Fulfillment on payment                                                              |
| ------------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `plan_id`    | Charges the plan's prices; line items and amount derive from the plan         | Customer account + active subscription + paid first invoice                         |
| `line_items` | Ad-hoc items (`description`, `quantity`, `unit_price`); amount is their total | Customer account + paid one-off invoice carrying those line items — no subscription |
| `amount`     | A bare total with no itemization                                              | Customer account + paid one-off invoice with a single line                          |

To store a payment method **without charging**, set `"checkout_intent": "vault"` and omit all three — the checkout collects and vaults a card for later use (for example, invoice autopay).

One-off invoices are created with zero tax so the invoice total always equals the amount the customer was charged. If you need tax on one-off purchases, calculate it before creating the session and include it in your line items.

### Who the customer is

Three identity modes, chosen by what you pass:

| You pass                                                                                          | When to use                               | Checkout behavior                                                   |
| ------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------- |
| `customer_id` or `customer_alias`                                                                 | The buyer is an existing Alguna customer  | Checkout skips identity entirely; saved payment methods are offered |
| `customer` object (`name`, `email`, optional `first_name`, `last_name`, `website_url`, `address`) | The buyer is signed in to your product    | Checkout pre-fills identity and goes straight to payment            |
| Nothing                                                                                           | Anonymous visitors (public pricing pages) | Checkout collects email and name itself before payment              |

The component adapts automatically — there is no client-side configuration for identity. The session is the source of truth.

`client_reference_id` is your own reference (order ID, cart ID). It is echoed on the session and on the `checkoutsession.completed` webhook so you can correlate completions with your records.

***

## Mounting the checkout

Install the SDK:

```bash theme={null}
npm install @alguna/checkout-sdk
```

### React

```tsx theme={null}
import { AlgunaCheckout } from "@alguna/checkout-sdk/react";

<AlgunaCheckout
  url={session.url}
  appearance={{ accentColor: "#4f46e5" }}
  onComplete={() => setPurchased(true)}
  onError={(e) => {
    if (e.code === "session_expired") refreshSession();
  }}
/>
```

### Any framework

```js theme={null}
import { initCheckout } from "@alguna/checkout-sdk";

const checkout = initCheckout({
  url: session.url,
  container: "#checkout",
  onComplete: () => showSuccess(),
});

// Later: checkout.update({ appearance: { mode: "dark" } });
// On teardown: checkout.destroy();
```

### Options

| Option                                            | Description                                                                                                                                                 |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`                                             | The session's hosted URL. Required.                                                                                                                         |
| `container`                                       | Element or selector to mount into (non-React only).                                                                                                         |
| `appearance`                                      | Theme configuration — see below. Updatable live without remounting.                                                                                         |
| `layout`                                          | `"compact"` removes the checkout's own heading and tightens padding for embedding in dense UI.                                                              |
| `maxHeight`                                       | Caps the component height in pixels; the form scrolls inside and the pay button stays pinned at the bottom. Without it the iframe grows to fit its content. |
| `onReady` / `onComplete` / `onError` / `onResize` | Lifecycle callbacks.                                                                                                                                        |

### Appearance

```ts theme={null}
appearance: {
  preset: "minimal" | "soft" | "sharp",   // starting point
  mode: "light" | "dark",
  accentColor: "#4f46e5",                  // pay button + interactive elements
  borderRadius: 10,
  fontFamily: "Inter, sans-serif",
  density: "comfortable" | "compact",
  payButtonLabel: "Upgrade for {amount}",  // {amount} is replaced with the total
  colors: { surface, card, text, mutedText, border, inputBackground },
}
```

All appearance changes apply live via `update()` (or by changing the React prop) without remounting or losing form state.

***

## Handling completion

`onComplete` fires in the browser the moment payment succeeds — use it to update your UI (show a success state, unlock a feature optimistically). **Do not treat it as proof of purchase**: browser events can be spoofed or lost.

Fulfill from the server using either:

* **Webhook** — subscribe to `checkoutsession.completed`. The payload carries the session `id`, `amount`, `currency`, `accountId`, `paymentId`, `invoiceId`, `subscriptionId` (plan sessions), and your `clientReferenceId`.
* **Polling** — `GET /beta/checkout-sessions/{id}` returns the session `status` plus `payment_id`, `subscription_id`, and `invoice_id` once fulfillment completes.

Fulfillment is asynchronous: the payment, account, and subscription or invoice attach within seconds of completion, not in the same instant the customer sees the success state.

***

## Session expiry

When a session expires before payment, the component reports `onError({ code: "session_expired" })`. Mint a fresh session from your server and update the component's `url`. Short-lived sessions (for example `expires_in_seconds: 3600`) are recommended for checkout links tied to a specific cart or price.

***

## Restricting where checkout can be embedded

Under **Settings → Workflows → Payments → Embedded checkout**, you can list the origins (e.g. `https://app.example.com`) allowed to embed your checkout. An empty list allows any origin.

This is a safeguard against accidental embedding — for example, a stale integration on a deprecated domain. It is not the security boundary: sessions are protected by their unguessable, expiring session tokens, and payment credentials never pass through the embedding page.

***

## Example: in-app upgrade

```ts theme={null}
// server — Node/Express
app.post("/api/upgrade-session", requireUser, async (req, res) => {
  const upstream = await fetch("https://api.alguna.io/beta/checkout-sessions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ALGUNA_API_KEY}`,
      "Alguna-Version": "2026-04-01",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      plan_id: "pln_033ZYEaFZURxhK5a1cUAgQ",
      customer: { name: req.user.company, email: req.user.email },
      client_reference_id: `upgrade_${req.user.id}`,
      expires_in_seconds: 3600,
    }),
  });
  res.json(await upstream.json());
});
```

```tsx theme={null}
// client — React
const { data: session } = useQuery(["upgrade-session"], createUpgradeSession);

{session && (
  <AlgunaCheckout
    url={session.url}
    layout="compact"
    appearance={{ accentColor: "#0d9488", payButtonLabel: "Upgrade for {amount}" }}
    onComplete={() => queryClient.invalidateQueries(["subscription"])}
  />
)}
```

On completion, the customer account exists in Alguna with an active subscription and a paid invoice, and your webhook endpoint receives `checkoutsession.completed` with `clientReferenceId: "upgrade_<id>"`.

***

## FAQ

**Can I change what a session charges after creating it?**
No. Sessions are immutable snapshots of what's being sold. Create a new session instead — they are cheap and expire on their own.

**How do saved payment methods work?**
Sessions created with `customer_id` for a customer with vaulted payment methods offer them automatically alongside new-card entry.

**What payment methods appear?**
The same methods as your hosted checkout, driven by your payment processor configuration and routing rules.

**Does the embedded component handle 3-D Secure?**
Yes — processor-driven authentication runs inside the component.

**Can I use this without the SDK?**
Yes. The session `url` is a standard hosted checkout page; you can redirect to it directly instead of embedding. The SDK adds embedding, theming, resizing, and lifecycle events.
