Skip to main content

Hosted Fields

Use the getHostedFields() method to render individual, PCI-compliant card input fields as secure iframes within your own custom form layout. Each field (card number, expiry, CVV…) is isolated inside a separate iframe while your page retains full control over the surrounding UI.

When to use this recipe
  • You want to build a fully custom card form UI (layout, labels, styles all yours).
  • You are collecting vault credit card data only.
  • You need PCI-DSS compliant field isolation without managing sensitive data yourself.
Vault credit card only

getHostedFields() is only available for vault credit card payment methods. For other methods, use getPaymentElement() instead. For a pre-built form that also handles theming, see Hosted Form.

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 form containers to your HTML​

Create placeholder div elements for each card field. The SDK will inject secure iframes into these targets:

<form id="payment-form">
<div class="field">
<label>Card Number</label>
<div class="row">
<div id="card-number"></div>
<div id="brand-selector"></div>
</div>
</div>

<div class="field">
<label>Cardholder Name</label>
<div id="cardholder-name"></div>
</div>

<div class="row">
<div class="field">
<label>Expiry Date</label>
<div id="expiry-date"></div>
</div>
<div class="field">
<label>CVV</label>
<div id="cvv"></div>
</div>
</div>

<button type="submit" id="submit-btn">Pay Now</button>
<div id="error"></div>
</form>

2. Render the hosted fields​

Subscribe to payment methods, retrieve the credit card method, call getHostedFields() with your target element IDs, then call render():

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

if (creditCard) {
const hostedFields = creditCard.getHostedFields({
fields: {
brandSelector: { target: "brand-selector" },
cardNumber: { target: "card-number", placeholder: "1234 5678 9012 3456" },
holderName: { target: "cardholder-name", placeholder: "John Doe" },
expDate: { target: "expiry-date", placeholder: "MM/YY" },
cvv: { target: "cvv", placeholder: "123" }
}
});

hostedFields.render();
}
});

💡 render() mounts one secure iframe per target element. The brandSelector field is optional — it displays a brand icon based on the card number entered.

3. Customize field appearance​

Pass a theme object to getHostedFields() to style the fields from the outside. Only global and input scopes are supported:

const hostedFields = creditCard.getHostedFields({
fields: { /* ...same as above... */ },
theme: {
global: {
fontFamily: 'Inter, sans-serif',
fontSize: '16px'
},
input: {
color: "#111827",
backgroundColor: "#ffffff",
':focus': { color: "#111827" },
':invalid': { color: "#dc2626" }
}
}
});
Theme scope reference

See Theme references for all available keys. For richer theming (labels, helper text, tooltips), use Hosted Form instead.

Custom fonts​

Contact required

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

Pre-approved Google Fonts (Inter, Lato, Montserrat, Noto, Nunito, Open Sans, Raleway, Roboto, Work Sans) are ready to use without approval — just set fontFamily:

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

4. Handle form submission​

Call checkout.submitPayment() on form submit:

document.getElementById("payment-form").addEventListener("submit", async (e) => {
e.preventDefault();

const submitBtn = document.getElementById("submit-btn");
const errorDiv = document.getElementById("error");

submitBtn.disabled = true;
submitBtn.textContent = "Processing...";
errorDiv.style.display = "none";

try {
await checkout.submitPayment();
// Redirect or show success message
} catch (error) {
errorDiv.textContent = error.message || "Payment failed";
errorDiv.style.display = "block";
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "Pay Now";
}
});

Error handling​

Common errors
  • Expired session → Create a new client session
  • Invalid field configuration → Check that all target element IDs exist in the DOM
  • Payment method not supported → The method does not support Hosted Fields (only vault credit card is supported)
  • 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 Fields</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }
.field { margin-bottom: 16px; }
label { display: block; margin-bottom: 4px; font-weight: 500; }
.input-container { border: 1px solid #ccc; border-radius: 4px; padding: 10px; height: 40px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
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; }
.error { color: #dc2626; font-size: 14px; margin-top: 8px; display: none; }
</style>
</head>
<body>
<form id="payment-form">
<div class="field">
<label>Card Number</label>
<div class="row">
<div id="card-number" class="input-container"></div>
<div id="brand-selector"></div>
</div>
</div>
<div class="field">
<label>Cardholder Name</label>
<div id="cardholder-name" class="input-container"></div>
</div>
<div class="row">
<div class="field">
<label>Expiry Date</label>
<div id="expiry-date" class="input-container"></div>
</div>
<div class="field">
<label>CVV</label>
<div id="cvv" class="input-container"></div>
</div>
</div>
<button type="submit" id="submit-btn">Pay Now</button>
<div id="error" class="error"></div>
</form>

<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;

const hostedFields = creditCard.getHostedFields({
fields: {
brandSelector: { target: "brand-selector" },
cardNumber: { target: "card-number", placeholder: "1234 5678 9012 3456" },
holderName: { target: "cardholder-name", placeholder: "John Doe" },
expDate: { target: "expiry-date", placeholder: "MM/YY" },
cvv: { target: "cvv", placeholder: "123" }
},
theme: {
input: {
fontSize: "16px",
color: "#111827",
':focus': { color: "#111827" },
':invalid': { color: "#dc2626" }
}
}
});

hostedFields.render();
});

document.getElementById("payment-form").addEventListener("submit", async (e) => {
e.preventDefault();
const submitBtn = document.getElementById("submit-btn");
const errorDiv = document.getElementById("error");
submitBtn.disabled = true;
submitBtn.textContent = "Processing...";
errorDiv.style.display = "none";
try {
await checkout.submitPayment();
} catch (error) {
errorDiv.textContent = error.message || "Payment failed";
errorDiv.style.display = "block";
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "Pay Now";
}
});
</script>
</body>
</html>

See also​

  • Brand Management — Detect and manage co-branded cards with getHostedFields()
  • Hosted Form — Use a pre-built form UI via getPaymentElement({ hostedForm })
  • Customization — Theming the default payment element with getPaymentElement()