Skip to main content

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
});
SDK Reference

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');
}
});
Payment is Already Complete

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');
}
}
});
Error Handling

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 pre-open a popup

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.

3D Secure May Interrupt

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​

HookWhenBlocks PaymentUse For
onBeforeValidateBefore API call✅ YesValidation, loading states, analytics
onAfterValidateAfter payment succeeds❌ NoCleanup, success tracking
redirectionHandlerWhen 3DS needed❌ NoCustom 3DS UI
delegate3DSChallengeOption❌ NoIn-browser challenge (Lyra, Axepta) in a popup

See PurseHeadlessCheckoutHooks for parameter types and RedirectionHandlerParams for redirection types.