Checkout Context
Checkout Context lets you initialise CityPay Elements before a Payment Intent has been created. This is useful when the shopper can still change their basket while the payment UI is visible.
For example, you can render card fields, Apple Pay, or Google Pay without creating a Payment Intent for every checkout page visit.
A Payment Intent is created only when the shopper commits to the payment.
Checkout Context does not make the browser authoritative for the order.
Your backend must still validate the final basket, amount, currency, stock, shipping, discounts, and other order details before creating the Payment Intent.
A Checkout Context payment has three stages:
- Your backend creates a Checkout Context.
- The browser initialises Elements and alternative payment methods (Apple Pay / Google Pay) using that context.
- When the shopper commits to pay, the SDK sends the current payment request through your exchange middleware, which creates the Payment Intent.
The important distinction is that rendering the payment component does not create a Payment Intent.
Initialise Elements with either a checkoutContext returned by your backend or a createCheckoutContext callback that the SDK can call when it needs a fresh context.
import { CityPay } from "@citypay/sdk";
const citypay = new CityPay("YourClientID", "YourLicenceKey", {
sandbox: true,
});
const elements = await citypay.elements({
pubKey: 'YOUR_PUBLIC_KEY',
createCheckoutContext: async () => {
const res = await fetch('/api/payments/checkout-context', {
method: 'POST',
})
if (!response.ok) {
throw new Error('Unable to create checkout context')
}
return response.json()
},
middleware: {
exchange: '/api/checkout/context/exchange',
},
})
createCheckoutContext runs when Elements needs a Checkout Context. Your backend should return the Checkout Context response produced by CityPay. Do not expose your server-side CityPay credentials to the browser.
citypay.checkoutContexts.cached, you can wrap the context callback and reuse an eligible context between page loads. Payment details should be supplied through paymentRequest. Use a function rather than a static object when the amount or other checkout details can change.
function getPaymentRequest() {
return {
total: {
label: 'Order total',
amount: 24.99,
currency: 'GBP',
country: 'GB',
},
identifier: 'order-12345',
merchantData: {
basketId: 'basket-12345',
},
}
}
The function is passed to the payment component:
paymentRequest: () => getPaymentRequest()
This lets the SDK request the current checkout values when the shopper commits to the payment.
paymentRequest are browser input. Validate them against your server-side basket before exchanging the Checkout Context. Apple Pay is created through expressCheckout.
const express = elements.expressCheckout({
element: '#wallet-container',
methods: ['apple_pay'],
paymentRequest: () => getPaymentRequest(),
appearance: {
applePay: {
type: 'check-out',
style: 'dark',
},
},
})
const available = await express.init()
if (available) {
await express.awaitReady()
}
express.init() checks whether the requested payment method can be made available in the current checkout.
If it returns false, do not assume Apple Pay is available. Continue to offer another supported payment method, such as card or Google Pay.
Handling Apple Pay events
You can listen for Elements events to update your checkout UI.
express.on?.('cpe:authorise:start', () => {
console.log('Apple Pay authorisation started')
})
express.on?.('cpe:authorise:end', (event) => {
if (!event?.success) {
console.error('Apple Pay authorisation failed')
return
}
console.log('Apple Pay authorised', event.detail?.authResponse)
})
The Checkout Context exchange is handled through the middleware.exchange configured when Elements was created. You do not need to manually create a Payment Intent from the Apple Pay button handler.
Google Pay uses the same expressCheckout API.
const express = elements.expressCheckout({
element: '#wallet-container',
methods: ['google_pay'],
paymentRequest: () => getPaymentRequest(),
environment: 'TEST',
appearance: {
buttonType: 'checkout',
buttonColor: 'black',
buttonSizeMode: 'fill',
},
emailAddressRequired: true,
billingAddressRequired: true,
})
const available = await express.init()
if (available) {
await express.awaitReady()
}
Use TEST while developing your Google Pay integration. Use the production environment only when your Google Pay configuration is ready for production.
Handing Google Pay events
The Google Pay Checkout Context example listens for cpe:tokenise:end.
express.on?.('cpe:tokenise:end', async (event) => {
const { paymentIntentId, status } = event.detail
if (status !== 'requires_authorisation') {
console.error('Payment is not ready for authorisation', status)
return
}
// Continue the normal Payment Intent authorisation flow.
await authorisePaymentIntent(paymentIntentId)
})
At this point the Checkout Context has been exchanged and the event contains the resulting Payment Intent information needed to continue the payment flow.
The exchange endpoint runs on your backend.
Configure it when creating Elements:
const elements = await citypay.elements({
pubKey: 'YOUR_PUBLIC_KEY',
createCheckoutContext,
middleware: {
exchange: '/api/checkout/context/exchange',
},
})
When the shopper commits to the payment, Elements calls this endpoint as part of the Checkout Context flow.
Your endpoint should:
- Identify the merchant basket or order.
- Load its current state from your backend.
- Recalculate and validate the amount.
- Validate currency, stock, shipping, discounts, and other checkout rules.
- Exchange the Checkout Context with CityPay using server-side credentials.
- Return the Payment Intent information expected by Elements.
Do not trust the amount received from the browser without comparing it with your backend state.