Skip to main content

Embeddable Checkout Widget

The bopen checkout widget is a self-contained, zero-dependency JavaScript component that merchants embed on their checkout pages. It handles the complete Open Banking A2A payment UX in a sandboxed iframe.

Architecture

MERCHANT PAGE (any origin)

│ <script src="/v1/checkout.js" data-checkout-token="..." async>

▼ (injects)
┌─────────────────────────────────────────────┐
│ SECURE IFRAME /widget/index.html │
│ │
│ Vanilla JS · No external dependencies │
│ Isolated from merchant DOM │
│ Communicates via window.postMessage only │
└─────────────────────────────────────────────┘

XSS isolation: merchant-page scripts cannot access the iframe DOM, input fields, or API payloads. Communication is exclusively via postMessage with origin checking.

Bundle size: loader + widget = 12 KB gzipped (well within the 20 KB target).

Merchant integration snippet

<!-- Payment container -->
<div id="bopenbanking-payment-widget"></div>

<!-- Loader script -->
<script
src="https://dev-checkout.bopenbanking.com/v1/checkout.js"
data-checkout-token="BASE64_ENCODED_TOKEN"
data-container-id="bopenbanking-payment-widget"
async>
</script>

Checkout token format

The data-checkout-token is a base64-encoded JSON object generated server-side:

{
"amount": "150.00",
"currency": "GBP",
"merchantName": "Acme Store",
"apiUrl": "https://dev.api.bopenbanking.com",
"returnUrl": "https://acme.example.com/checkout/success",
"merchantClientId": "acme-corp-client-id",
"callbackOrigin": "https://dev-checkout.bopenbanking.com",
"payerName": "John Smith",
"debtorAccountScheme": "UK.OBIE.SortCodeAccountNumber",
"debtorIdentification": "20000012345678",
"remittanceReference": "INV-2026-001"
}

Payer context fields (payerName, debtorAccountScheme, debtorIdentification, remittanceReference) are optional. They are stored on payment_consents for AML compliance.

Screen state machine

The widget progresses linearly through five screens. The user cannot go backward past ST-03.

[ST-01: Consent] → [ST-02: Bank] → [ST-03: Handover] → [ST-04: Processing] → [ST-05: Receipt]
IDScreenKey elements
ST-01Consent DisclosureExact OBL legal text: "By clicking 'Pay with Bank', you explicitly authorize bopenbanking to securely route a one-off payment of £X from your bank account to [Merchant]." Amount, merchant name. "Cancel and Return to Merchant" link.
ST-02Bank DirectoryQuick Pay tile for returning users (last-used bank from localStorage). Popular banks grid. Full searchable list (16 UK banks). HTML-injection-safe search filter. "Cancel" link.
ST-03Handover GatewayMobile: "Open [Bank] App" deep-link button. Desktop: Canvas QR code (self-hosted RS error-correction encoder, no external library). Trust copy: "We will never see your online banking passwords." "Cancel" link removed after handover click.
ST-04Processing LoopAnimated circular spinner. Full interaction lock: beforeunload listener + history.pushState override prevents back-button and tab-close during polling (2 500 ms intervals, 2-minute max). No cancel option.
ST-05ReceiptSuccess: green ✓, end_to_end_id reference, gross/fee/net breakdown. Failure: friendly error message mapped from failure_code (e.g. "Insufficient funds in your account"), "Retry Transaction" button loops back to ST-02.

Fast-path (skip ST-01 + ST-02)

If the token includes debtorAccountScheme + debtorIdentification, the widget skips consent and bank selection entirely — the user goes straight to ST-03 using their preferred bank from localStorage (or the first available bank). Optimises conversion for returning customers where the merchant already knows the payer's account.

Quick Pay (ST-02)

On second and subsequent visits, the widget reads bopen_bank_preference from localStorage on the widget's origin. If found, it displays a "⚡ Quick Pay" tile at the top of ST-02 with the last-used bank pre-selected. On successful payment, the preference is stored:

{
"lastUsedBankId": "barclays",
"lastUsedTimestamp": "2026-07-08T10:45:22Z",
"payerFingerprint": "fp_sha256_a8f3c21b..."
}

Lifecycle events

Listen for events on the merchant page:

window.addEventListener("message", function(event) {
if (event.origin !== "https://dev-checkout.bopenbanking.com") return;

const { eventType, payload } = event.data;

switch (eventType) {
case "BOPEN_WIDGET_READY": // widget rendered
break;
case "BOPEN_BANK_SELECTED": // payload.bankId, payload.bankName
break;
case "BOPEN_PAYMENT_SUCCESS": // payload.transactionId, payload.endToExternalId
// clear cart, redirect
break;
case "BOPEN_PAYMENT_FAILED": // payload.errorCode
break;
case "BOPEN_PAYMENT_CANCELLED": // user clicked "Cancel and return"
break;
}
});

Regulatory safeguards

  • "Cancel and Return to Merchant" link is prominent in ST-01, ST-02, ST-03. Clicking it fires BOPEN_PAYMENT_CANCELLED and navigates the parent to returnUrl.
  • No credential harvesting — the widget never renders fields for card PANs, PINs, or online banking passwords. It discovers the bank and hands the payer over to the bank's native application layer.
  • Navigation lock at ST-04beforeunload + history.pushState override prevents the payer from accidentally double-submitting by hitting Back.

Files

FilePurpose
apps/web/public/v1/checkout.jsLoader — reads data-* attributes, injects iframe, relays postMessage + BOPEN_RESIZE
apps/web/public/widget/index.html5-screen widget — all JS + CSS inline, zero external dependencies, self-hosted QR encoder
apps/web/public/widget-demo.htmlMerchant integration demo — live event log, adjustable parameters

Demo

Not currently deployed

The widget files above exist in the repository and build correctly, but as of this writing they have not been deployed to the live dev-checkout.bopenbanking.com Amplify app — that app's most recent deployment predates the widget's creation, so /widget/index.html, /v1/checkout.js, and /widget-demo.html all currently return 404 on the live site. Verified: curl -I https://dev-checkout.bopenbanking.com/widget-demo.html404.

To deploy it: ./infra/cloudformation/scripts/deploy-web-amplify.sh (see Getting Started). Until that runs, use the demo locally instead:

cd apps/web && npm run dev

Then open http://localhost:3000/widget-demo.html. Set amount, currency, and API URL, click Launch widget to preview all 5 screens with a live event log.