Skip to main content

Hosted Form

Use getPaymentElement() with the hostedForm option to render a pre-built, PCI-DSS compliant payment form directly inside your checkout page. The SDK renders a complete, styled form — you only configure labels, placeholders, and a theme. Choose this approach when you want a quick integration with moderate customization and support for multiple payment methods. For full control over each individual field's position and HTML structure, use Hosted Fields instead.

Prerequisites

This recipe assumes you have a checkout instance already initialized. See the Getting Started guide if you haven't done this yet.


1. Add a container element to your HTML

Add a single container where the SDK will mount the complete form:

<div id="purse-hosted-form"></div>
<button id="pay-button">Pay Now</button>

2. Render the Hosted Form

Subscribe to payment methods, then call getPaymentElement() with the hostedForm option and mount it with appendTo():

checkout.paymentMethods.subscribe((methods) => {
const creditCard = methods.find(m => m.method === "creditcard");

if (creditCard) {
creditCard.getPaymentElement({
hostedForm: {
panInputLabel: "Card number",
panPlaceholder: "1234 5678 9012 3456",
cvvInputLabel: "Security code",
cvvPlaceholder: "123",
expirationInputLabel: "Expiration date",
expirationPlaceholder:"MM/YY",
holderInputLabel: "Cardholder name",
holderPlaceholder: "John Doe"
}
}).appendTo("purse-hosted-form");
}
});

💡 appendTo("purse-hosted-form") mounts the complete form into the element with that ID.

3. Customize appearance

Labels and placeholders

All field labels and placeholder texts are configurable via the hostedForm object (see step 2 above).

Theme

Pass a theme object alongside hostedForm to control the visual styling. The Hosted Form supports a richer set of theme scopes than Hosted Fields:

creditCard.getPaymentElement({
hostedForm: { /* ...labels and placeholders... */ },
theme: {
global: {
color: '#1f2933',
fontFamily: 'Arial, sans-serif',
fontSize: '16px',
gap: '8px'
},
input: {
padding: '12px',
borderRadius: '4px',
border: '1px solid #cbd5e0',
backgroundColor: '#ffffff',
':focus': { borderColor: '#4299e1', boxShadow: '0 0 0 3px rgba(66,153,225,0.1)' },
':invalid': { borderColor: '#dc2626' }
},
label: {
fontWeight: '500',
margin: '0 0 4px 0',
color: '#374151'
},
helperText: {
fontSize: '14px',
color: '#6b7280',
margin: '4px 0 0 0'
}
}
}).appendTo("purse-hosted-form");

Supported theme scopes: global, input, label, helperText, tooltip. Each scope supports standard CSS properties and pseudo-selectors (:focus, :hover, :invalid…).

See Theme references for all available keys.

Custom fonts

Contact required

For compliance reasons, please contact the Purse team before using custom fonts — you must provide the font file in advance.

theme: {
global: {
fontSrc: 'custom-font-file', // only needed for non-pre-approved fonts
fontFamily: 'MyFont, sans-serif',
fontSize: '16px'
}
}

4. Handle payment submission

Call checkout.submitPayment() on button click:

document.getElementById("pay-button").addEventListener("click", async (e) => {
e.preventDefault();

const payButton = document.getElementById("pay-button");
payButton.disabled = true;
payButton.textContent = "Processing...";

try {
await checkout.submitPayment();
// Redirect or show success message
} catch (error) {
console.error("Payment failed:", error.message);
alert(error.message || "Payment failed");
} finally {
payButton.disabled = false;
payButton.textContent = "Pay Now";
}
});

Error handling

Common errors
  • Expired session → Create a new client session
  • Invalid configuration → Check that the hostedForm options are correctly set
  • Payment method not supported → The method does not support the Hosted Form rendering mode
  • Technical error → The payment method could not be initialized

Complete example

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hosted Form</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
#purse-hosted-form { margin-bottom: 20px; }
button { width: 100%; padding: 12px; background: #0066cc; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
</style>
</head>
<body>
<div id="purse-hosted-form"></div>
<button id="pay-button">Pay Now</button>

<script type="module">
import * as Purse from "https://cdn.purse-sandbox.com/headless-checkout/latest/purse.esm.js";

const checkout = await Purse.createHeadlessCheckout(clientSession.widget.data);

checkout.paymentMethods.subscribe((methods) => {
const creditCard = methods.find(m => m.method === "creditcard");
if (!creditCard) return;

creditCard.getPaymentElement({
hostedForm: {
panInputLabel: "Card number",
panPlaceholder: "1234 5678 9012 3456",
cvvInputLabel: "Security code",
cvvPlaceholder: "123",
expirationInputLabel: "Expiration date",
expirationPlaceholder:"MM/YY",
holderInputLabel: "Cardholder name",
holderPlaceholder: "John Doe"
},
theme: {
global: { color: '#1f2933', fontFamily: 'Arial, sans-serif', fontSize: '16px' },
input: { padding: '12px', borderRadius: '4px', border: '1px solid #cbd5e0', ':focus': { borderColor: '#4299e1' } }
}
}).appendTo("purse-hosted-form");
});

document.getElementById("pay-button").addEventListener("click", async (e) => {
e.preventDefault();
const payButton = e.target;
payButton.disabled = true;
payButton.textContent = "Processing...";
try {
await checkout.submitPayment();
} catch (error) {
alert(error.message || "Payment failed");
} finally {
payButton.disabled = false;
payButton.textContent = "Pay Now";
}
});
</script>
</body>
</html>

See also

  • Hosted Fields — Render isolated card input iframes with getHostedFields() for full layout control
  • Customization — Theme the default payment element with getPaymentElement()
  • Event Handling — React to checkout events during the payment flow