Embedded checkout

Embedded checkout lets sellers offer checkout on their own website. The seller backend creates a short-lived checkout session with a secret API key. The browser receives only the session token and can only perform the checkout actions allowed by that session.

Use embedded checkout when moving from legacy PlanVariant and Subscription forms to the V2 product, price, bundle and contract model.

Overview

A normal integration has four parts:

  1. Configure V2 catalog products, prices or bundle templates.

  2. Create a sales channel that controls which offers, origins and payment methods are available.

  3. Create a checkout session from the seller backend.

  4. Mount the askell.js version for embedded checkout with the returned session token.

The secret API key must stay on the seller backend.

Create a session

POST /api/v2/checkout-sessions/
Authorization: Api-Key your-secret-api-key
Content-Type: application/json
{
  "sales_channel": "memberships",
  "customer_reference": "seller-user-123",
  "terms_url": "https://seller.example/terms",
  "terms_required": true,
  "metadata": {
    "seller_cart_id": "cart-456"
  },
  "expires_in_seconds": 1800
}

Response:

{
  "token": "cs_abc123",
  "status": "pending",
  "expires_at": "2026-06-16T12:30:00Z",
  "sales_channel": "memberships",
  "allowed_origins": ["https://seller.example"],
  "theme": {
    "primary_color": "#0F766E"
  }
}

The token is safe to send to the browser. It does not replace the seller API key and cannot be used to call normal private V2 APIs.

If terms are configured on the sales channel, you do not need to send terms_url or terms_required when creating a session. These fields are only used when a single session should use different terms than the sales channel.

Mount the widget

<div id="askell-checkout"></div>
<script src="https://cdn.askell.is/js/dist/askell.js"></script>
<script>
  Askell.mountCheckout("#askell-checkout", {
    sessionToken: "SESSION_TOKEN_FROM_SELLER_BACKEND",
    title: "Veldu áskrift",
    description: "Veldu áskriftarleið og viðbætur áður en gengið er frá greiðslu.",
    language: "is",
    colorScheme: "auto",
    onSuccess(result) {
      window.location.href = "/account?checkout=success";
    },
    onError(error) {
      console.error(error);
    }
  });
</script>

askell.js automatically loads its companion askell.css stylesheet from the same directory as the script itself. If the script is self-hosted, serve askell.css from the same location.

Use title to customize the widget heading. If title is omitted, the widget uses its default heading.

Use description to show short help text below the heading on the first checkout step. If description is omitted, no extra text is shown.

Use language to select the widget text language. Supported values are "is" and "en". If language is omitted, the widget tries to use the page lang attribute, otherwise it uses English.

Use colorScheme to select a light or dark widget appearance. Supported values are "light", "dark" and "auto". "auto" follows the browser prefers-color-scheme setting. The default value is "light". You can also use color_scheme if the seller backend uses snake_case.

Browser API

The browser SDK uses session-token endpoints:

GET  /api/v2/checkout-sessions/{token}/public/
POST /api/v2/checkout-sessions/{token}/quote/
POST /api/v2/checkout-sessions/{token}/payment-processor-options/
POST /api/v2/checkout-sessions/{token}/checkout/
POST /api/v2/checkout-sessions/{token}/payment-method-registrations/
GET  /api/v2/checkout-sessions/{token}/payment-method-registrations/{registration_token}/
POST /api/v2/checkout-sessions/{token}/finalize/

The token is passed in the URL path for these browser endpoints. The browser cannot broaden the session policy, change customer binding, set arbitrary amounts or submit unapproved metadata.

Sales channel setup

Sales channels are reusable checkout profiles. They control what a buyer can purchase and from where checkout may be used.

Add-ons are loaded automatically from active add-on rules for selected products and bundles. The sales channel does not need to select add-on products separately; disable add-ons on the sales channel only when they should not be shown.

If prices are alternatives for the same product, for example monthly and yearly prices, set offer_policy.price_selection_mode to "single". The widget then displays prices as options where the buyer chooses one price, and the API rejects requests that try to buy more than one base price. The default value is "multiple" and keeps the previous behavior where the buyer can select more than one price.

The sales channel also controls how the widget gets the customer reference with customer_reference_setting. "kennitala" shows a field for an Icelandic ID number and validates it before binding the customer to the session. "random" hides the field and the widget generates a random UUID as the reference.

If the selected product or a product in a bundle is not marked as electronic, the widget collects the buyer address. The buyer can also choose a different delivery address. When the country is Iceland, the widget shows a searchable Icelandic postcode field and resolves the city from the postcode. For other countries, the widget shows a free-form postcode field and a separate city field. The widget sends delivery_address with the checkout request when a different delivery address is selected, and the delivery address is saved on the contract when checkout is completed.

Before activating a sales channel, confirm:

  • products are active

  • prices are active

  • bundle templates have active items and valid default prices

  • selected currencies and billing schedules are compatible

  • allowed origins are configured

  • allowed collection methods are configured

  • payment processor policy is either empty, a valid allowlist, or one selected account payment processor

  • metadata policy only allowlists harmless frontend tracking keys

Allowed origins must be origins only:

https://seller.example
http://localhost:3000

Do not include paths, query strings, credentials or wildcards:

https://seller.example/checkout
https://user:pass@seller.example
https://*.seller.example

Terms

A sales channel can define terms that the buyer must accept before checkout is created. This is usually configured once on the sales channel:

{
  "terms_policy": {
    "url": "https://seller.example/terms",
    "required": true
  }
}

terms_policy.url is the URL of the seller’s terms page. terms_policy.required means the buyer must accept the terms before they can pay. If url is configured, the widget shows a link next to the acceptance checkbox.

Terms can be overridden for a single checkout session with terms_url and terms_required in POST /api/v2/checkout-sessions/. Existing sessions keep a snapshot of the terms policy, so create a new session after changing the sales channel.

Checkout theme is not owned by the sales channel. The widget uses a built-in default theme, but the seller backend can pass a per-session override and the widget JavaScript setup can override individual theme values.

Theme variables

Checkout themes are sanitized and snapshotted when a checkout session is created. Seller backends can pass a per-session override in the checkout session request:

{
  "sales_channel": "memberships",
  "customer_reference": "seller-user-123",
  "theme": {
    "primary_color": "#0F766E",
    "accent_color": "#2563EB",
    "background_color": "#FFFFFF",
    "surface_color": "#F8FAFC",
    "text_color": "#0F172A",
    "muted_text_color": "#475569",
    "border_color": "#CBD5E1",
    "error_color": "#DC2626",
    "success_color": "#15803D",
    "border_radius": "md",
    "font_family": "system"
  }
}

The embedded widget applies the resolved theme as CSS variables on the checkout root element:

Theme field

CSS variable

Purpose

primary_color

--askell-primary-color

Primary buttons and highlighted actions.

accent_color

--askell-accent-color

Secondary accents and supporting highlights.

background_color

--askell-background-color

Checkout page background.

surface_color

--askell-surface-color

Product cards, panels and confirmation surfaces.

text_color

--askell-text-color

Primary text.

muted_text_color

--askell-muted-text-color

Secondary text and descriptions.

border_color

--askell-border-color

Borders and dividers.

error_color

--askell-error-color

Error states.

success_color

--askell-success-color

Success states.

border_radius

--askell-border-radius

Widget corner radius.

font_family

--askell-font-family

Widget font stack.

Color values must be valid hex colors, either #RGB or #RRGGBB. Unsupported theme fields are rejected. border_radius supports none, sm, md and lg. font_family supports system, inter and inherit.

The API also checks basic contrast rules: text must contrast with the background and surface colors, muted text must contrast with the background, and primary_color must contrast with white button text.

Hosted payment frames may not inherit every visual setting from the widget. The checkout shell uses the full theme, while payment method registration applies the sanitized session theme where the selected processor supports it.

Security checklist

  • Store secret keys only in server-side settings or environment variables.

  • Create sessions from trusted server-side code.

  • Bind sessions to customer or customer_reference when checkout is customer-specific.

  • Configure allowed origins before activating a sales channel.

  • Keep session lifetimes short.

  • Allowlist frontend metadata keys explicitly.

  • Verify the created contract server-side before granting paid access.

The browser must not receive:

  • ASKELL_SECRET_KEY

  • private API keys

  • account payment processor secrets

  • unrestricted product, price, amount, accounting, contract or migration fields

  • unrestricted metadata

Frontend metadata

Server metadata can be attached when the seller backend creates the session. Frontend metadata is accepted only when the sales channel allowlists the keys:

{
  "metadata_policy": {
    "frontend_allowed_keys": ["utm_source", "campaign"]
  }
}

Do not allow frontend keys that affect accounting, entitlement, contract references, migration source ids, processor selection or customer identity.

Django backend example

With django-askell:

from django.http import JsonResponse
from askell.client import client


def create_askell_session(request):
    result = client.create_checkout_session(
        sales_channel="memberships",
        user=request.user,
        metadata={"source": "pricing-page"},
        expires_in_seconds=1800,
    )
    if result["status"] != "success":
        return JsonResponse(
            {"error": result["response"]},
            status=result["status_code"],
        )
    return JsonResponse({"sessionToken": result["response"]["token"]})

The package also exposes a logged-in helper view at /askell/checkout-session/ when the package URLs are included. You can subclass the view and set sales_channel if the channel should be fixed server-side.

Migration from legacy plan forms

For every legacy PlanVariant exposed in a seller frontend, map it to one of:

  • a V2 CatalogProduct and CatalogPrice

  • a V2 BundleTemplate

  • an initial one-time CatalogPrice

Then create a sales channel that exposes only the mapped V2 offers and replace the old frontend form with embedded checkout.

Legacy access checks should move from “active legacy subscription for plan variant” to “active V2 subscription contract item for product or price”. Do not grant access based only on the browser callback; verify the resulting contract server-side.

Admin-operated migration of existing live legacy subscriptions is documented in the internal migration runbook. Embedded checkout only changes how new buyers enter the V2 contract model.

Troubleshooting

Session returns 404

Common causes:

  • token is wrong or stale

  • session is expired

  • session is completed or canceled

  • frontend is using a token from another environment

Create a fresh session and confirm the token, status and expiry.

Origin mismatch

If checkout works locally but fails in production, check the sales channel allowed origins. Existing sessions keep their snapshotted origin policy, so create a new session after changing the sales channel.

Offer rejected

Confirm the selected price, product, bundle or add-on appears in the public session offer_catalog. If it does not, update the sales channel offer policy and create a new session.

Payment processor unavailable

Check that the selected or pinned account payment processor supports the currency and collection method. Non-card collection methods require a customer with the required verified payment method.

Payment method registration fails

Create checkout before registering a payment method, use the registration token returned by the session endpoint, and poll the session-scoped registration status endpoint until it reaches a terminal state.

Finalize fails

Confirm the session is still pending, the checkout belongs to the session, and payment method registration succeeded. Retrying finalize is safe after a completed session because the endpoint returns the idempotent result.

Payment pages and sales channels

Payment pages can optionally attach a sales channel. Legacy payment pages without a sales channel keep their page-specific settings. When a sales channel is attached, V2 bundle payment pages use the sales channel bundle allowlist, collection methods and payment processor policy.