Hooks
Hooks let you tap into the payment flow to add custom behavior like loading states, analytics tracking, or custom 3D Secure handling.
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
onBeforeValidate: async (splits) => { /* your code */ },
onAfterValidate: async () => { /* your code */ },
redirectionHandler: async (data) => { /* your code */ },
delegate3DSChallenge: false
});
For complete type definitions, see PurseHeadlessCheckoutHooks interface.
Show a Loading Indicator During Payment
Use onBeforeValidate and onAfterValidate to control UI state.
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
onBeforeValidate: async () => {
// Disable submit button and show loader
document.querySelector('#pay-button').disabled = true;
document.querySelector('#loader').classList.remove('hidden');
},
onAfterValidate: async () => {
// Hide loader (payment succeeded)
document.querySelector('#loader').classList.add('hidden');
}
});
When onAfterValidate runs, the payment has already been processed. Errors here won't stop the payment.
Track Payment Events in Analytics
Send payment events to your analytics platform.
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
onBeforeValidate: async (splits) => {
// Track payment start
gtag('event', 'begin_checkout', {
value: splits.reduce((sum, s) => sum + s.amount, 0) / 100,
currency: 'EUR'
});
},
onAfterValidate: async () => {
// Track payment success
gtag('event', 'purchase', { transaction_id: 'your-transaction-id' });
}
});
Validate Data Before Submitting
Block the payment if validation fails.
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
onBeforeValidate: async () => {
// Check terms acceptance
const termsAccepted = document.querySelector('#terms-checkbox').checked;
if (!termsAccepted) {
throw new Error('Please accept terms and conditions');
}
}
});
Throwing an error in onBeforeValidate stops the payment. The API call won't be made.
Handle 3D Secure Authentication
Customize how 3D Secure authentication is displayed to users.
Default Behavior (Full Page Redirect)
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
redirectionHandler: async ({ url, type, method, JSONPayload }) => {
if (type === 'jsonFormSubmit') {
// Create and auto-submit form (full page redirect)
const form = document.createElement('form');
form.method = method;
form.action = url;
const data = JSON.parse(JSONPayload || '{}');
Object.entries(data).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = String(value);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
}
});
Custom Behavior (Modal or iFrame)
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
redirectionHandler: async ({ url, type }) => {
if (type === 'redirection') {
// Open 3DS in a centered popup window
const width = 500;
const height = 600;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
window.open(
url,
'3DS Authentication',
`width=${width},height=${height},left=${left},top=${top}`
);
}
}
});
In-Browser Challenge in a Popup
Some partners (Lyra, Axepta) run their 3D Secure challenge in the browser instead of redirecting: the challenge is drawn in a modal inside the checkout. If your checkout lives in a narrow iframe, that modal ends up cramped. Set delegate3DSChallenge to show the challenge in a popup instead.
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
delegate3DSChallenge: true
});
- The checkout opens a popup on a page it generates, which hosts the bank's authentication page. Once the buyer has authenticated, the popup closes by itself.
- If the browser blocks the popup, the challenge shows in a dialog inside the checkout instead.
- If the buyer closes the popup before authenticating, the authentication fails and the checkout follows the payment redirection.
- When no challenge is needed, no popup opens.
With a redirectionHandler, the final redirection after a challenge shown in the popup reaches it as a redirection with challengedInPopup: true. Follow it in place: the buyer has already seen a popup.
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
delegate3DSChallenge: true,
redirectionHandler: async (data) => {
if (data.type === 'redirection' && data.challengedInPopup) {
window.location.href = data.url;
return;
}
// ... your usual redirection handling
}
});
Do not open your own popup on the Pay click for this flow: the checkout opens its own, and the buyer would get two windows. Partners that already redirect for their challenge are not affected.
If 3D Secure is required, redirectionHandler runs instead and the user must complete authentication first. Plan your redirect flow accordingly.
Complete Example
Here's a full integration combining loading states, analytics, and 3D Secure handling:
const checkout = await Purse.createHeadlessCheckout('your-session-data', {
// Before payment: Validate and prepare UI
onBeforeValidate: async (splits) => {
// Validate terms acceptance
if (!document.querySelector('#terms').checked) {
throw new Error('Please accept terms and conditions');
}
// Update UI
document.querySelector('#pay-button').disabled = true;
document.querySelector('#loader').classList.remove('hidden');
// Track event
gtag('event', 'begin_checkout', {
value: splits.reduce((sum, s) => sum + s.amount, 0) / 100
});
},
// After payment: Cleanup and track success
onAfterValidate: async () => {
document.querySelector('#loader').classList.add('hidden');
gtag('event', 'purchase');
},
// Handle 3DS if required
redirectionHandler: async ({ url, type, method, JSONPayload }) => {
if (type === 'jsonFormSubmit') {
const form = document.createElement('form');
form.method = method;
form.action = url;
const data = JSON.parse(JSONPayload || '{}');
Object.entries(data).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = String(value);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
}
}
);
Best Practices
- Keep hooks fast: They run during the payment flow. Avoid slow operations.
- Use
async/await: Always return promises from hooks. - Validate in
onBeforeValidate: This is your last chance to stop a payment. - Handle errors gracefully: In
onAfterValidate, remember the payment already succeeded.
Hook Reference
| Hook | When | Blocks Payment | Use For |
|---|---|---|---|
onBeforeValidate | Before API call | ✅ Yes | Validation, loading states, analytics |
onAfterValidate | After payment succeeds | ❌ No | Cleanup, success tracking |
redirectionHandler | When 3DS needed | ❌ No | Custom 3DS UI |
delegate3DSChallenge | Option | ❌ No | In-browser challenge (Lyra, Axepta) in a popup |
See PurseHeadlessCheckoutHooks for parameter types and RedirectionHandlerParams for redirection types.