# Embedded checkout

> Open Borga checkout as a modal on your own page with borga.js, using a publishable key and a per-session client secret.

Source: https://docs.borga.is/payments/embedded-checkout

Embedded checkout keeps the payer on your page. Your server creates a session in `embedded` mode and hands two values to the browser: the session id and a one-time `client_secret`. The borga.js script opens a modal that loads the checkout, and calls back into your page when the payment completes.

Under the hood the modal loads `checkout.borga.is` in an iframe, and the card fields live in the card processor's iframe inside it. Your page never sees card data, and neither does Borga: the PCI boundary is the same as hosted checkout.

> **Note**
> Embedded checkout supports cards, Apple Pay and Google Pay. Bank invoices (krafa) need the hosted page.

**Create a publishable key**

Under **Developers**, create a **publishable** key and add the origins your checkout pages run on, for example `https://shop.example.is`. Origins are scheme and host only, no path. `localhost` and `127.0.0.1` are always allowed for development.

**Create an embedded session on your server**

Use your secret key. Set `mode` to `embedded` and `origin` to the page that will open the modal. The response includes a `client_secret` (`pcs_…`) that is only ever returned once.

```ts Node.js
// POST /api/checkout  (your server)
export async function POST(request: Request) {
  const { cartId } = (await request.json()) as { cartId: string };
  const amount = await totalForCart(cartId);

  const session = await borga.paymentSessions.create({
    amount,
    currency: "ISK",
    mode: "embedded",
    origin: "https://yoursite.is", // the page that will open the modal
    external_reference: cartId,
  });

  // Only these two values go to the browser. Never the secret key.
  return Response.json({
    sessionId: session.id,
    clientSecret: session.client_secret,
  });
}

declare function totalForCart(cartId: string): Promise<number>;
```

Return only `id` and `client_secret` to the browser. The amount is locked on the server; nothing the browser sends can change it.

**Open the modal in the browser**

Load borga.js from `https://js.borga.is/v1/borga.js`, construct a client with your publishable key, and call `checkout.open` with the two values from your server.

```html HTML + JS
<script src="https://js.borga.is/v1/borga.js" async></script>
<button id="pay">Pay 12.900 kr.</button>

<script>
  const borga = new Borga("pk_test_…");

  document.getElementById("pay").addEventListener("click", async () => {
    // Ask your server for a session (see the server snippet).
    const { sessionId, clientSecret } = await fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ cartId: "cart_123" }),
    }).then((r) => r.json());

    borga.checkout.open({
      sessionId,
      clientSecret,
      onComplete: ({ paymentId }) => {
        window.location.href = `/thank-you?payment=${paymentId}`;
      },
      onCancel: () => console.log("Payer closed the modal"),
      onError: (err) => console.error(err.message),
    });
  });
</script>
```
```tsx React
"use client";

import { useBorgaCheckout } from "@borga/react";

export function PayButton({ cartId }: { cartId: string }) {
  const checkout = useBorgaCheckout({
    publishableKey: process.env.NEXT_PUBLIC_BORGA_PUBLISHABLE_KEY!,
    onError: (err) => console.error(err.message),
  });

  async function pay() {
    const { sessionId, clientSecret } = await fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ cartId }),
    }).then((r) => r.json() as Promise<{ sessionId: string; clientSecret: string }>);

    checkout.open({
      sessionId,
      clientSecret,
      onComplete: ({ paymentId }) => {
        window.location.href = `/thank-you?payment=${paymentId}`;
      },
    });
  }

  return <button onClick={pay}>Pay 12.900 kr.</button>;
}
```

`open()` returns a handle with `close()` if you need to dismiss the modal yourself. Pressing Escape or clicking the backdrop closes it and fires `onCancel`.

**Fulfil from the webhook**

`onComplete` fires when the checkout reports success and is the right moment to move the payer to a confirmation page. Ship the order from the [`payment.succeeded` webhook](/webhooks), which fires whether or not the browser is still open.

## Callbacks

| Callback | When | Payload |
| --- | --- | --- |
| `onComplete` | The payment succeeded | `{ paymentId, status: "succeeded" }` |
| `onCancel` | The payer closed the modal without paying | none |
| `onError` | The session could not load, or the payment failed in a way the payer cannot retry | `{ message, code? }` |

## Creating sessions from the browser

A publishable key can also create embedded sessions directly from the browser by calling `POST /v1/payment_sessions` with `Authorization: Bearer pk_test_…`. The browser's `Origin` header must be in the key's allowed origins, and only `mode: "embedded"` is accepted. Since the amount then comes from the browser, use this only when the server-side amount is verified later, for example by comparing the webhook payload against your order before fulfilling.

## 3-D Secure

Most challenges complete inside the modal. When an issuer insists on a full-page redirect, the checkout returns to `checkout.borga.is/…/3ds-return` and the modal continues automatically. You do not need to handle this.

## Content Security Policy

If your site sets a CSP, allow these origins on the page that opens checkout:

```
script-src  'self' https://js.borga.is;
frame-src   https://checkout.borga.is;
connect-src 'self' https://api.borga.is;
```

Add `https://*.straumur.is` to `frame-src` and `connect-src` if you see the card form blocked; the card processor loads its own iframe inside the checkout page.

## React

`@borga/react` wraps the script loader in a hook, `useBorgaCheckout`, that returns `open` and `close`. It loads borga.js once per page and is marked `"use client"`. The package is built from the Borga monorepo alongside `@borga/node`; until it is on npm, install it from the repository or copy the hook, which is about 80 lines with no dependencies beyond React.

## Framework-agnostic

borga.js is a plain script that sets `window.Borga`. Vue, Svelte, Angular and vanilla pages use it the same way as the HTML example above. Load it only in the browser: importing it during server rendering throws, because it touches `document`.
