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.
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.
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.localhostand127.0.0.1are always allowed for development.Create an embedded session on your server
Use your secret key. Set
modetoembeddedandoriginto the page that will open the modal. The response includes aclient_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
idandclient_secretto 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 callcheckout.openwith 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>Reacttsx"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 withclose()if you need to dismiss the modal yourself. Pressing Escape or clicking the backdrop closes it and firesonCancel.Fulfil from the webhook
onCompletefires when the checkout reports success and is the right moment to move the payer to a confirmation page. Ship the order from thepayment.succeededwebhook, 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.