# Hosted checkout

> Redirect the payer to a Borga-hosted page that handles cards, wallets, 3-D Secure and bank invoices.

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

Hosted checkout is the simplest integration and supports every payment method. Your server creates a payment session, you redirect the payer to its `url` on `checkout.borga.is`, Borga collects the payment, and the payer is sent back to your `return_url`. Card details never touch your servers, which keeps you in the lightest PCI scope (SAQ A).

**Create the session**

Hosted is the default mode. `return_url` and `cancel_url` are required.

```ts Node.js
const session = await borga.paymentSessions.create(
  {
    amount: 12900,
    currency: "ISK",
    customer_email: "anna@example.is",
    external_reference: "order_5678",
    return_url: "https://yoursite.is/order/complete",
    cancel_url: "https://yoursite.is/cart",
    enabled_methods: ["card", "apple_pay", "google_pay"],
    locale: "is",
    metadata: { order_id: "5678" },
  },
  // Your own idempotency key makes retries across processes safe too.
  { idempotencyKey: "order_5678_checkout" },
);

// Send the payer to the hosted page.
redirect(session.url!);
```
```bash curl
curl https://api.borga.is/v1/payment_sessions \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_5678_checkout" \
  -d '{
    "amount": 12900,
    "currency": "ISK",
    "customer_email": "anna@example.is",
    "external_reference": "order_5678",
    "return_url": "https://yoursite.is/order/complete",
    "cancel_url": "https://yoursite.is/cart",
    "enabled_methods": ["card", "apple_pay", "google_pay"],
    "locale": "is",
    "metadata": { "order_id": "5678" }
  }'
```

Sessions stay `open` for 24 hours. Send an `Idempotency-Key` (the SDK does automatically) so a retried request cannot create two sessions; see [Idempotency](/api/idempotency).

**Redirect the payer**

Send the browser to `session.url` with an HTTP 303 redirect from your server, or set `window.location.href` in the browser. The payer sees your merchant name and logo, the amount, wallet buttons when available, the card form, and a bank-invoice tab if enabled.

**Handle the return**

On success Borga shows a confirmation for about two seconds and then redirects to `return_url` with `session=ps_…` appended as a query parameter. Read the session and branch on `payment_status`:

```ts Node.js
// GET /order/complete?session=ps_…  (Next.js App Router route handler)
export async function GET(request: Request) {
  const sessionId = new URL(request.url).searchParams.get("session");
  if (!sessionId) return new Response("Missing session", { status: 400 });

  const session = await borga.paymentSessions.retrieve(sessionId);

  switch (session.payment_status) {
    case "succeeded":
      return Response.redirect(`/thank-you?order=${session.id}`, 303);
    case "created":
    case "processing":
      // Bank invoices and slow 3-D Secure flows land here. Show a
      // "we're confirming your payment" page and finish on the webhook.
      return Response.redirect("/order/pending", 303);
    default:
      return Response.redirect("/order/failed", 303);
  }
}
```

Treat the return page as a status page. The authoritative signal is the [`payment.succeeded` webhook](/webhooks), which also covers payers who never make it back to your site.

**Handle cancellation and failure**

If the payer uses the back link, Borga sends them to `cancel_url` unchanged. If a card is declined, the checkout shows the error and lets the payer try again; the session stays open. If the session expires, the page offers a link to `cancel_url`. In all of these cases no payment succeeded, and there is nothing to clean up on Borga's side.

## Session options

The most useful fields. The full list is in the [API reference](/api/payment-sessions).

| Field | Purpose |
| --- | --- |
| `amount`, `currency` | What to charge. `currency` defaults to `ISK`. |
| `return_url`, `cancel_url` | Where the payer lands afterwards. HTTPS on an allowed domain in live mode; `http://localhost` is fine in test mode. |
| `external_reference` | Your order or invoice number. Copied to the Payment and to webhook payloads so you can match them. |
| `metadata` | Any JSON object. Copied to the Payment. |
| `customer_email` | Pre-fills the email field and locks it. |
| `customer` | Attach the payment to an existing Customer, needed for saved cards and subscriptions. |
| `enabled_methods` | Restrict or extend the methods shown. Default `["card", "apple_pay", "google_pay"]`; add `"bank_invoice"` to offer krafa. |
| `locale` | `is` (default) or `en`. Other language tags fall back to English. |
| `save_payment_method` | Save the card for later charges. See [Saved cards](/payments/saved-cards). |
| `subscription` | Start a subscription with these prices on success. See [Subscriptions](/billing/subscriptions). |

## Allowed redirect domains

Borga only redirects to domains you have registered, so a compromised integration cannot send payers to a phishing page. Add them under **Settings → Account → Allowed redirect domains**. Matching is on the hostname exactly, so list `shop.example.is` and `www.shop.example.is` separately if you use both. Attempts to use other domains fail at session creation with [`redirect_url_not_allowed`](/errors/redirect_url_not_allowed).

## Branding

The merchant name, logo and primary colour shown on the checkout come from **Settings → Account**. The page is served in Icelandic by default and in English when `locale` is `en`.

## Payment links without code

Every session `url` is a shareable payment link valid for 24 hours. Creating sessions from a small internal tool, or straight from an invoicing workflow, is a common way to take payments by email or chat before building a full checkout.
