Embedded checkout

Open Borga checkout as a modal on your own page with borga.js, authenticated by a session id and a one-time client secret.

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. Those two values are what authenticate the checkout. The borga.js script opens a modal that loads it, 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.

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

    Node.jsts
    // 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.

  2. Open the modal in the browser

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

    HTML + JShtml
    <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>

    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. Only one checkout is open at a time: opening a second one closes the first and cancels it.

    The publishable key the examples pass to new Borga(…) is optional; see Publishable keys below.

  3. Fulfil from the webhook

    onComplete fires once Borga has confirmed the payment with the card processor, so the Payment normally reads back as succeeded from your server at that moment. If the processor's status lookup is unavailable right then, the callback still fires on the card form's own result and the Payment can read back as processing until the processor's webhook settles it. It is the right moment to move the payer to a confirmation page. Ship the order from the payment.succeeded webhook, which fires whether or not the browser is still open.

Callbacks

CallbackWhenPayload
onCompleteThe payment completed. Borga normally confirms it first, but the Payment can still be processing if confirmation is unavailable{ paymentId, status: "succeeded" }
onCancelThe payer closed the modal without payingnone
onErrorThe session could not load, or the payment failed in a way the payer cannot retry{ message, code? }

Awaiting the result

The value open() returns is both the handle and a promise of the outcome, so you can await it instead of passing callbacks:

const result = await borga.checkout.open({ sessionId, clientSecret });

if (result.status === "complete") {
  window.location.href = `/thank-you?payment=${result.paymentId}`;
} else if (result.status === "error") {
  console.error(result.error.message);
}

status is "complete", "canceled" or "error". The promise resolves for every outcome, a failed payment included, and never rejects, so no try/catch is needed. The callbacks still fire, so the two styles mix freely. @borga/react's open() returns the same result.

Publishable keys

new Borga(publishableKey) accepts a pk_test_… / pk_live_… key, but the key is optional and is not what authenticates the modal: the session id and its one-time client_secret are. new Borga() opens checkout just as well; a key that is passed is validated for the pk_ prefix and otherwise reserved for origin pairing and analytics. The same goes for useBorgaCheckout({ publishableKey }) in @borga/react.

A publishable key is required for the pattern below, where the browser creates the session itself. To create one, go to 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.

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.

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;

That is all borga.js needs: the checkout page inside the frame calls the API and loads the card processor's fields under its own policy, so your page makes no other requests for it. Add connect-src https://api.borga.is only if your page calls the API itself, as in Creating sessions from the browser.

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". Install it with npm install @borga/react. It has no dependencies beyond React. Its open() takes the same per-call callbacks and returns the same awaitable result as borga.checkout.open().

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.