Skip to main content

Hosted form (Web SDK)

Set mode: 'hosted_form' on initSecureFields to render a complete, ready-styled card form — card number, expiry date, CVV and cardholder name — into a single container. You supply texts and a theme; the SDK owns the labels, the layout and the brand selector. Each field is still its own cross-origin iframe under the hood, so card data never touches your page — exactly as in hosted fields mode. What you gain is not having to build and style the form yourself; what you give up is control over the arrangement of the fields.

Want full layout control instead?

Use hosted fields mode (the default, mode: 'hosted_fields') — you place and style each field yourself. Native apps use the iOS SDK or Android SDK, which don't yet offer a hosted form equivalent.

1. Add a container element to your HTML​

The SDK replaces the content of this single element with the whole form:

<div id="secure-fields-form"></div>
<button id="pay-button">Pay</button>

2. Initialize in hosted form mode​

import { loadSecureFields } from '@purse-eu/web-sdk';

const sf = await loadSecureFields('sandbox'); // or 'production'

const secureForm = await sf.initSecureFields({
tenantId: '${VAULT_TENANT_ID}',
config: {
mode: 'hosted_form',
container: 'secure-fields-form', // an element id, not a css selector — same convention as `target` in hosted fields mode
brands: ['VISA', 'MASTERCARD', 'CARTE_BANCAIRE', 'AMERICAN_EXPRESS'],
hostedForm: {
panInputLabel: 'Card number',
panPlaceholder: '1234 5678 9012 3456',
cvvInputLabel: 'CVV',
expirationInputLabel: 'Expiry date',
holderInputLabel: 'Cardholder name',
},
theme: {
global: { color: '#181818', fontSize: '16px' },
input: { borderRadius: '4px', ':focus': { borderColor: '#0070f3' } },
},
},
});

secureForm.render();

hostedForm and theme mirror the hostedForm and theme options accepted by getPaymentElement() in the Purse widget key for key — a merchant already integrating the widget can reuse the exact same two objects here.

3. Customize the displayed texts​

Every key in hostedForm is optional — anything you omit falls back to a shipped English default.

hostedForm: {
panInputLabel: 'Card number',
panPlaceholder: '1234 5678 9012 3456',
panRequiredError: 'The card number is required',
panFormatError: 'The card number is invalid',
panTooltipText: 'The 16 digits on the front of your card',

expirationInputLabel: 'Expiry date',
expirationPlaceholder: 'MM/YY',
expirationRequiredError: 'The expiry date is required',
expirationFormatError: 'Invalid date',
expirationOutOfRangeError: 'This card has expired',

cvvInputLabel: 'CVV',
cvvPlaceholder: '123',
cvv4InputLabel: 'CVV (4 digits)', // used for cards with a 4-digit CVV, e.g. Amex
cvv4Placeholder: '1234',
cvvRequiredError: 'The CVV is required',
cvvFormatError: 'Invalid CVV',
cvvTooltipText: 'The 3-digit code on the back of your card',

holderInputLabel: 'Cardholder name',
holderPlaceholder: 'John Doe',
holderRequiredError: 'The cardholder name is required',
holderFormatError: 'Invalid cardholder name',

noCVV: false, // set to true to hide the CVV field entirely
hideHolderName: false, // set to true to hide the cardholder name field entirely
}
info

HostedFormUIOptions also accepts a handful of keys used only by the Purse widget (iban*, email*, phone*, consent*, loanSimulation). Secure Fields ignores them — they exist so the same object type-checks against both products.

4. Customize appearance with a theme​

The theme axis is scope × css key × state, with five scopes: global, input, label, helperText and tooltip. States (:focus, :invalid, :valid, :hover, :disabled, ::placeholder…) are expressed as variants on each scope rather than as separate token buckets.

theme: {
global: {
color: '#1f2933',
fontFamily: 'Helvetica, Arial, sans-serif',
fontSize: '15px',
gap: '12px',
},
input: {
color: '#181818',
backgroundColor: '#ffffff',
borderRadius: '6px',
borderColor: '#c7ccd4',
borderStyle: 'solid',
borderWidth: '1px',
padding: '10px',
':focus': { borderColor: '#0070f3', borderWidth: '2px' },
':invalid': { borderColor: '#d32f2f', color: '#d32f2f' },
':valid': { borderColor: '#0f8613' },
'::placeholder': { color: '#999999' },
},
label: { fontSize: '14px', fontWeight: '600', color: '#344054' },
helperText: {
fontSize: '12px',
color: '#667085',
':invalid': { color: '#d32f2f' },
},
}
Contact required for custom fonts

For compliance reasons, please contact the Purse team before using a custom font — you must provide the font file in advance, then reference it with global.fontSrc.

5. Card brand selection​

hostedForm.brandSelectionMode controls how a co-badged card (e.g. Visa/Carte Bancaire) is presented:

ModeBehavior
implicit (default)The detected brand is shown as a badge inside the card number field, with a native select over it for co-badged cards. Selection happens inside the field — nothing else to wire up.
explicitA labelled select is rendered on its own row below the form. Customize its label with hostedForm.explicitBrandText.
hiddenNo selector is rendered. Listen for brandDetected and call setSelectedBrand() from your own UI, exactly as in manual brand selection for hosted fields.

Override the displayed name of one or more brands with hostedForm.brandLabels:

hostedForm: {
brandSelectionMode: 'explicit',
explicitBrandText: 'Card brand',
brandLabels: { CARTE_BANCAIRE: 'CB' },
}

In every mode, the selected network drives the card number's digit grouping and the accepted CVV length, and is the brand the card is tokenized under. implicit and explicit need no wiring for this. In hidden mode, tell the form which brand was chosen:

secureForm.on('brandDetected', ({ brands }) => renderMyBrandChoices(brands));

myBrandSelect.addEventListener('change', (e) => {
secureForm.setSelectedBrand(e.target.value);
});
note

Call setSelectedBrand() as the choice is made, not at submit — the card number field reformats in response to it. A brand the BIN lookup did not detect is ignored.

6. Listen for events and submit​

secureForm.on('ready', () => {
// The form has been rendered and is ready for input
});

secureForm.on('brandDetected', ({ brands }) => {
console.log('Detected brands:', brands);
});

secureForm.on('formValid', ({ hasErrors }) => {
document.getElementById('pay-button').disabled = hasErrors;
});

try {
const result = await secureForm.submit({
saveToken: false, // set to true to save the card for future payments
});

// Send result.vault_form_token to your backend
console.log('vault_form_token:', result.vault_form_token);
} catch (err) {
// The form also paints validation errors onto itself — this is for your own handling
console.error('Submit failed:', err.code, err.reason);
}

See Events for the full list of available events and their payloads, and Create a payment for what to do with vault_form_token on your backend.

Update texts or theme after render​

Call setConfig() to replace the texts or the theme without recreating the form:

secureForm.setConfig({
hostedForm: { panInputLabel: 'Numéro de carte' },
});

secureForm.setConfig({
theme: { global: { color: '#3b0764' } },
});

Each call is merged into what's already in effect, not a full replacement — panInputLabel above only changes that one text, and every other hostedForm and theme value you set earlier (including the shipped defaults) stays as it was.

Next steps​