π Hosted Fields SDK
Secure SDK for embedding payment input fields (card number, expiry date, CVV, etc.) inside iframes. It is designed to ensure PCIβcompliance by isolating sensitive data from the merchant's site. Supports new card payments, paying with previously saved cards (vaulted tokens), bank transfer payments, and Apple Pay / Google Pay wallet payments via Evervault.
π Usage
-
Include the SDK on your checkout page (currently available via CDN only):
<!-- Sandbox --> <script src="https://engine-sandbox.pay.tech/hf/sdk/lib/umd/index.js"></script> <!-- Production --> <script src="https://engine.pay.tech/hf/sdk/lib/umd/index.js"></script>Inject these URLs via your server-side config or a build-time variable β never hardcode production URLs in source code.
-
Initialize the SDK with your paymentId:
const sdk = HostedFieldsSDK({ paymentId: 'payment_id_required', theme: 'dark' }); // optional: show a loading skeleton while the SDK fetches session data sdk.renderLoadingSkeleton('#loading-container', 4);Config options:- paymentId (string, required) β your payment session ID.
- theme ('light' | 'dark', optional) β initial color theme for all hosted fields. Defaults to
'light'. If you pass customfieldStylesand use the dark theme, make sure to also providefieldStyles.darkoverrides β otherwise your light-mode colors will persist in dark mode.
renderLoadingSkeletonrenders animated placeholder rows inside the given container. Call it right after creating the SDK instance β beforeinit()β so users see a loading state instead of blank space. The skeleton is replaced once you mount the actual fields inonReady. -
Prepare containers for the fields on your page:
<!-- shown while the SDK loads, hidden once fields are mounted --> <div id="loading-container"></div> <form autocomplete="on" onsubmit="submitPayment(event)"> <div id="cardNumber" style="height: 3.5rem;"></div> <div style="display: flex; gap: 1rem;"> <div id="expiryDate" style="height: 3.5rem; flex: 1;"></div> <div id="cvv" style="height: 3.5rem; flex: 1;"></div> </div> <div id="cardholderName" style="height: 3.5rem;"></div> <button type="submit">Submit</button> </form>Each field container needs an explicit height (e.g.
3.5rem) β without it the iframe will collapse and the field won't be visible.The
idyou give to the loading container is the selector you pass torenderLoadingSkeleton. Hide the container yourself once the fields are mounted (see step 4).If you support saved cards, you will need two separate screens instead of a single form. See the Saved Cards section for the full HTML structure.
-
Mount the payment fields inside
onReady. Define your callbacks and helper functions before callingsdk.init()βonReadyfires onceinit()successfully loads the session data:sdk.onReady(() => { loadCheckoutData(); }); // If you support saved cards, check sdk.getSavedCards() here // and show the saved cards screen instead β see the Saved Cards section. function loadCheckoutData() { document.getElementById('loading-container').style.display = 'none'; // fieldStyles is optional β the SDK has built-in default styles. // Pass fieldStyles only if you want to customize colors, borders, etc. // If you use a dark theme with custom styles, also provide fieldStyles.dark overrides. sdk.mountField('#cardNumber', { fieldType: 'cardNumber', label: 'Card Number', fieldStyles: { variant: 'outlined', labelPosition: 'floating' } }); sdk.mountField('#expiryDate', { fieldType: 'expiryDate', label: 'Expiry Date', fieldStyles: { variant: 'outlined', labelPosition: 'floating' } }); sdk.mountField('#cvv', { fieldType: 'cvv', label: 'Card Security Code', fieldStyles: { variant: 'outlined', labelPosition: 'floating' } }); sdk.mountField('#cardholderName', { fieldType: 'cardholderName', label: 'Cardholder Name', fieldStyles: { variant: 'outlined', labelPosition: 'floating' } }); } sdk.onError((err) => console.error('SDK Error:', err)); sdk.onFieldValid((info) => console.log('Field valid:', info)); -
Start the SDK:
sdk.init(); -
Tokenize on form submission:
async function submitPayment(e) { e.preventDefault(); // prevent the form from reloading the page try { const result = await sdk.handleSubmit({ customerEmail: '[email protected]', billingCountryCode: 'US' }); if (result?.redirectUrl) window.location.replace(result.redirectUrl); } catch (error) { console.error('SDK error on submit:', error); } }
π Available Methods
HostedFieldsSDK(config)
Creates a new SDK instance. Call as a regular function β do not use new.
Parameters:
- paymentId (string, required) β the
paymentIdyou received after creating the payment.
Checkout session requirements:
- Payment method must be
BASIC_CARDorBANKTRANSFER. - State must be
CHECKOUT. - Both
amount(non-zero) andcurrencymust be set on the payment.
The fields you need to mount depend on the payment method. Check it via
sdk.getCheckoutInfo().paymentMethodinsideonReadyβ see.getCheckoutInfo()below.
.init()
Initializes the SDK and validates your payment id. Must be called before mounting fields.
.getCheckoutInfo()
Returns basic info about the current checkout session. Available right after .init() resolves (call it inside or after onReady).
Use paymentMethod to decide which fields to mount β 'BASIC_CARD' needs cardNumber, expiryDate, cvv, cardholderName; 'BANKTRANSFER' needs only customerAccountNumber.
Returns: { amount: number, currency: string, paymentMethod: 'BASIC_CARD' | 'BANKTRANSFER' }
Example
sdk.onReady(() => {
const { paymentMethod } = sdk.getCheckoutInfo();
if (paymentMethod === 'BANKTRANSFER') {
sdk.mountField('#customerAccountNumber', {
fieldType: 'customerAccountNumber',
label: 'Account Number'
});
} else {
sdk.mountField('#cardNumber', { fieldType: 'cardNumber', label: 'Card Number' });
sdk.mountField('#expiryDate', { fieldType: 'expiryDate', label: 'Expiry Date' });
sdk.mountField('#cvv', { fieldType: 'cvv', label: 'Card Security Code' });
sdk.mountField('#cardholderName', { fieldType: 'cardholderName', label: 'Cardholder Name' });
}
});
.onError(callback)
Registers a callback to handle initialization or runtime errors. The callback receives an error object with a message.
β οΈ Register
onErrorbefore callinginit(). Errors frominit()(invalidpaymentId, network failures, invalid checkout state) only reach this callback if it's already registered β otherwise they fall back toconsole.errorand you have no way to surface them to the user.
.onFieldValid(callback)
Registers a callback that is triggered when a field becomes valid (e.g. after the user corrects an invalid input). The callback receives an object with fieldType β the type of the field that became valid ('cardNumber', 'expiryDate', 'cvv', 'cardholderName', or 'customerAccountNumber').
.mountField(containerSelector, options)
Injects a secure payment input field (inside an iframe) into the specified container on your page.
β οΈ Calling
mountFieldwith afieldTypethat's already mounted is a no-op β the existing field stays as-is and your new options/styles are ignored. To re-mount with different options, callunmountField(fieldType)first.
Parameters:
- containerSelector (string, required) β CSS selector for a container
divin your HTML. - options (object, required):
- fieldType (string, required) β the type of payment field to render:
- For
paymentMethod: BASIC_CARD:'cardNumber','expiryDate','cvv','cardholderName'. - For
paymentMethod: BANKTRANSFER:'customerAccountNumber'β plain text, up to 64 characters, not tied to card capture; mount it instead of the four card fields.
- For
- label (string, optional) β label text displayed on the input.
- cardBrand (string, optional) β pre-set the card brand for CVV validation. Use when mounting a CVV field for a saved card, where the brand is already known and the
cardNumberfield is not mounted. Accepts the same brand strings assetCardBrand(). - fieldStyles (object, optional) β customize appearance:
- variant β
'standard'or'outlined' - labelPosition β
'floating'or'above' - fieldWrapper β custom CSS for the field wrapper
- inputBase β custom CSS for the input
- invalidInputBase β styles applied when the input is invalid
- labelBase β custom CSS for the label
- invalidLabelBase β styles applied to the label when input is invalid
- dark (object, optional) β overrides applied only in dark theme. Accepts the same keys:
fieldWrapper,inputBase,labelBase,invalidInputBase,invalidLabelBase. Layered on top of base styles when the theme is'dark'. If nodarkoverrides are provided, the base custom styles remain active in both themes.
- variant β
- fieldType (string, required) β the type of payment field to render:
Example with dark overrides
sdk.mountField('#cardNumber', {
fieldType: 'cardNumber',
label: 'Card Number',
fieldStyles: {
inputBase: {
backgroundColor: '#fff',
border: '1px solid #D0D5DD',
color: '#1D2939',
},
labelBase: {
color: '#667085',
backgroundColor: '#fff',
},
dark: {
inputBase: {
backgroundColor: 'transparent',
border: '1px solid #475467',
color: '#F5F5F6',
},
labelBase: {
color: '#98A2B3',
backgroundColor: '#1D2939',
},
},
},
});
.unmountField(fieldType)
Removes a mounted field from the DOM and resets its internal state. The field can be re-mounted into a different container afterwards.
Parameters:
- fieldType (string, required) β the type of field to remove:
'cardNumber','expiryDate','cvv','cardholderName', or'customerAccountNumber'.
Example
sdk.unmountField('cvv');
.unmountFields(fieldTypes)
Removes multiple mounted fields at once. Convenience wrapper around .unmountField().
Parameters:
- fieldTypes (string[], required) β array of field types to remove.
Example
sdk.unmountFields(['cardNumber', 'expiryDate', 'cvv', 'cardholderName']);
.handleSubmit(additionalFields)
Collects card data from mounted fields, validates it, and submits the payment.
Parameters:
- additionalFields (object, optional) β additional data to include in the payment payload. Keys must be valid field names; values are strings.
- paymentMethod ('APPLEPAY' | 'GOOGLEPAY', optional) β set this to submit an Apple Pay or Google Pay wallet payment instead of a card. See Apple Pay & Google Pay (via Evervault) below.
- applePay / googlePay (object, optional) β required when
paymentMethodis set to the matching wallet value. Pass the payment method object exactly as returned by Evervault'sprocesscallback β the SDK reshapes it internally, no transformation needed on your side.
Available field names:
saveCardβ (string:'true'|'false') tokenize and save the card for future payments (new card flow). Note: the value is a string, not a boolean.selectedCardIdβ (string) ID of a saved card to pay with; only the CVV field value is collected, all other card fields are ignored.customerFirstName,customerLastName,customerDateOfBirth,customerEmail,customerCitizenshipCountryCode,customerPhone,customerPersonalIdcustomerBankCode,customerBankBranchCodecustomerBank,customerAccountNamecustomerAccountTypeβ (string:'CHECKING'|'SAVINGS')billingCountryCode,billingAddressLine1,billingAddressLine2,billingCity,billingState,billingPostalCodedocumentType,documentNumber,pin
β οΈ
customerAccountNumberis not part ofadditionalFields. It is a hosted field β mount it withsdk.mountField()like any other field (see above) and its validated value is collected and included in the submit payload automatically.
Validation behavior:
- Before anything else, the SDK checks that the required fields are mounted:
paymentMethod: BASIC_CARDβcardNumber,expiryDate,cvv, andcardholderNamefor a new card, or justcvvwhenselectedCardIdis passed.paymentMethod: BANKTRANSFERβ justcustomerAccountNumber.- If any required field is missing,
handleSubmitrejects with'Missing required field(s): ...'without contacting the backend β this prevents submitting a payment with no data at all.
- On submission, all mounted fields are validated. If any are invalid the SDK marks them with an error state, focuses the first invalid field, and rejects with
'Please fill in all required fields correctly.' - If the checkout session has BIN restrictions, the card is validated against them after submission (
BASIC_CARDonly). Invalid brand rejects with'Card number brand is not valid.'; invalid issuing country rejects with'Card number country is not valid.' - For
paymentMethod: BANKTRANSFER, the submitted payload has nobasicCardobject at all β it containscustomerAccountNumberplus your other top-level fields. - When you pass a wallet
paymentMethod('APPLEPAY'/'GOOGLEPAY') with the matchingapplePay/googlePaytoken, none of the above field checks apply β no hosted card fields need to be mounted at all.
Returns: a promise with the payment result. The response always includes a redirectUrl β navigate the customer to it to complete the flow (e.g. 3-D Secure or success page).
Example β BASIC_CARD
const result = await sdk.handleSubmit({
customerEmail: '[email protected]',
billingCountryCode: 'US',
billingCity: 'New York'
});
Example β BANKTRANSFER
only customerAccountNumber is mounted, no card fields
const result = await sdk.handleSubmit({
customerEmail: '[email protected]',
billingCountryCode: 'US',
billingCity: 'New York',
customerBank: 'Bank of Example',
customerAccountName: 'John Doe',
customerAccountType: 'CHECKING'
});
Example β Apple Pay / Google Pay
paymentMethod object is whatever Evervault's process callback received β see Apple Pay & Google Pay (via Evervault)
const result = await sdk.handleSubmit({
paymentMethod: 'GOOGLEPAY',
googlePay: paymentMethod
});
.setTheme(theme)
Dynamically switches the color theme for all mounted hosted fields.
Parameters:
- theme ('light' | 'dark', required) β the theme to apply.
Example
sdk.setTheme('dark');
.setCardBrand(brand)
Manually sets the card brand for the CVV field. Use this when paying with a saved card β since the cardNumber field is not mounted, the brand cannot be detected automatically. The brand determines the expected CVV length (e.g. Amex requires 4 digits).
Parameters:
- brand (string, required) β card brand identifier (e.g.
'visa','mastercard','amex').
Example
sdk.setCardBrand('visa');
.getSavedCards()
Returns the list of saved cards for the current customer.
β οΈ Only call this method inside or after
onReadyβ the session data is not loaded untilinit()completes. Calling it earlier will return an empty array.
Returns: Array<{ id: number, brand: string, panMasked: string, expiryMonth: number, expiryYear: number }>
Example
const cards = sdk.getSavedCards();
// [{ id: 42, brand: 'visa', panMasked: 'β’β’β’β’ 4242', expiryMonth: 12, expiryYear: 2027 }]
.deleteCard(cardId)
Sends a delete request for the specified saved card via the payment provider API.
Parameters:
- cardId (number | string, required) β the ID of the card to delete.
Returns a Promise that resolves on success or rejects on failure (also triggers the onError callback).
Example
await sdk.deleteCard(42);
.renderLoadingSkeleton(containerSelector, count, height?)
Renders animated loading skeleton placeholders inside a container. Useful while the SDK is initializing or while switching between screens. Skeletons automatically reflect the current theme (light / dark).
Parameters:
- containerSelector (string, required) β CSS selector of the container to render skeletons into.
- count (number, required) β number of skeleton rows to render.
- height (string, optional) β height of each skeleton row (e.g.
'3.5rem','4rem'). If omitted, the skeleton fills its container via CSS β control the height by setting it on the container element itself.
Example
sdk.renderLoadingSkeleton('#form-fields', 4);
sdk.renderLoadingSkeleton('#card-row', 1, '4rem');
β οΈ Errors
The SDK surfaces errors in two ways:
- Promise rejections from async methods (
init,handleSubmit,deleteCard) βawaitthem inside atry/catch. - The
onErrorcallback β fired for both async failures and runtime issues that have no calling promise (e.g. a hosted-field iframe failing to load). Register it beforeinit()so it catches initialization errors.
Errors from async methods reach onError and reject the returned promise β handle whichever fits your flow, but do not duplicate UI feedback.
Common errors
| Message | Thrown / fired by | When it happens |
|---|---|---|
Invalid or unauthorized merchant token |
init() |
paymentId is missing or empty in the SDK config. |
Invalid checkout state or method |
init() |
The checkout session does not meet the requirements: state must be CHECKOUT, payment method must be BASIC_CARD or BANKTRANSFER, and both amount and currency must be set. |
HostedFields not initialized. Call .init() first. |
mountField(), handleSubmit(), deleteCard() |
Method was called before init() resolved. Call SDK methods inside or after onReady. |
Container not found: <selector> |
mountField() |
The CSS selector passed to mountField doesn't match any element in the DOM. Check that the container exists and the selector is correct. |
Missing required field(s): <fieldType, ...> |
handleSubmit() |
One or more required fields were never mounted: for BASIC_CARD β cardNumber, expiryDate, cvv, cardholderName for a new card, or just cvv when selectedCardId is passed; for BANKTRANSFER β customerAccountNumber. Mount them before calling handleSubmit. |
Please fill in all required fields correctly. |
handleSubmit() |
One or more mounted fields are invalid. The SDK marks them with an error state and auto-focuses the first invalid one. |
Card number brand is not valid. |
handleSubmit() |
The card brand is not in the checkout's allowed BIN brands. Only relevant when the merchant has BIN restrictions configured. |
Card number country is not valid. |
handleSubmit() |
The card's issuing country is rejected by the checkout's BIN country restrictions. |
Field "<fieldType>" failed to load |
onError only |
The hosted-field iframe failed to load (network error, CSP blocking, etc.). The container shows a fallback "Failed to load β please refresh" placeholder. |
For network failures from deleteCard() and the underlying form-submission step in handleSubmit(), the rejection error comes directly from the API and is also passed to onError.
Recommended pattern
sdk.onError((err) => {
// surface to the user (toast, banner, etc.)
showErrorToast(err.message);
});
sdk.onReady(() => {
mountFields();
});
sdk.init();
async function submitPayment(e) {
e.preventDefault();
try {
const result = await sdk.handleSubmit({ /* ... */ });
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
// already surfaced via onError β no need to duplicate
console.error('Submit failed:', error);
}
}
π³ Saved Cards
The SDK supports paying with previously saved cards. The general flow is:
- After
onReady, callsdk.getSavedCards()to get the customer's saved cards. - Render the card list in your UI (brand, masked number, expiry).
- When the user selects a card β mount the CVV field, then call
sdk.setCardBrand(brand). - Call
sdk.handleSubmit({ selectedCardId })to pay with the selected card.
To offer saving a new card, pass saveCard: 'true' to handleSubmit (the value is a string, not a boolean).
A CVV hosted field is required for saved card payments. Even though the card number and expiry are already stored on the backend, the customer must re-enter their CVV on every transaction. You must mount a
cvvfield and ensure it is valid before callinghandleSubmitβ otherwise the SDK will reject the submission.The CVV value never touches your server. It is captured inside an isolated iframe and sent directly to the payment provider by the SDK. Your page has no access to the raw value at any point. This is what keeps the integration PCI-compliant β you render the UI, the SDK handles all sensitive data.
Styling is fully yours. The saved cards list is plain HTML rendered by the merchant β the SDK only provides the data (
id,brand,panMasked,expiryMonth,expiryYear). You have full control over layout, card item design, selected state, delete button, icons, and animations. The only hosted element is the CVV field itself, which is an iframe styled viafieldStyles.
Key SDK operations
Selecting a card β unmount any previous CVV iframe, mount a fresh one inside the selected card's container, then set the brand so the CVV field knows the expected code length (3 digits for most brands, 4 for Amex):
sdk.unmountField('cvv');
sdk.mountField('#cvv-container', {
fieldType: 'cvv',
label: 'Card Security Code',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' },
});
sdk.setCardBrand(brand);
β οΈ Always unmount CVV before mounting it again for a different card. Without this, the previously typed CVV value stays in the field β the user may not notice and accidentally pay with the wrong code.
Paying with a saved card β pass selectedCardId to handleSubmit. You do not need to pass cardNumber, expiryDate, or cardholderName β those come from the saved card on the backend. The SDK uses the CVV value from the mounted CVV field automatically.
const result = await sdk.handleSubmit({
selectedCardId,
billingCountryCode: 'CY',
});
Deleting a card β deleteCard() automatically updates the local list, so getSavedCards() reflects the change immediately. If the deleted card was the selected one, unmount its CVV.
await sdk.deleteCard(cardId);
const updatedCards = sdk.getSavedCards();
β οΈ Deletion is irreversible. The SDK has no built-in confirmation step β you can call
sdk.deleteCard()directly on a button click, or wrap it in your own confirmation modal for safety. Both options are fine; the modal is recommended for production.
Example with a custom confirmation modal:
function onDeleteClick(card) {
// open your own modal β the SDK doesn't render one
showConfirmModal({
title: 'Delete card?',
message: `${card.brand.toUpperCase()} β’β’β’β’ ${card.panMasked.slice(-4)} will be removed.`,
onConfirm: async () => {
await sdk.deleteCard(card.id);
renderCardList(sdk.getSavedCards());
},
});
}
Switching screens β when going from the saved-cards screen to the new-card form, unmount the saved-card CVV and mount the four basic fields. To go back, unmount all four (including CVV) before re-rendering the saved cards list:
// β new card form
sdk.unmountField('cvv');
sdk.mountField('#cardNumber', { fieldType: 'cardNumber', label: 'Card Number' });
sdk.mountField('#expiryDate', { fieldType: 'expiryDate', label: 'Expiry Date' });
sdk.mountField('#cvv', { fieldType: 'cvv', label: 'CVV' });
sdk.mountField('#cardholderName', { fieldType: 'cardholderName', label: 'Cardholder Name' });
// β back to saved cards
sdk.unmountFields(['cardNumber', 'expiryDate', 'cvv', 'cardholderName']);
For a complete working implementation that ties all of this together, see the "Full example: saved cards + basic card" entry under Examples below.
π¦ Bank Transfer
For paymentMethod: BANKTRANSFER checkout sessions, mount only the customerAccountNumber field β none of the four BASIC_CARD fields apply.
<div id="loading-container"></div>
<form autocomplete="on" onsubmit="submitPayment(event)">
<div id="customerAccountNumber" style="height: 3.5rem;"></div>
<button type="submit">Submit</button>
</form>
const sdk = HostedFieldsSDK({ paymentId: '<payment_id>', theme: 'dark' });
sdk.renderLoadingSkeleton('#loading-container', 1);
sdk.onReady(() => {
const { paymentMethod } = sdk.getCheckoutInfo();
document.getElementById('loading-container').style.display = 'none';
if (paymentMethod === 'BANKTRANSFER') {
sdk.mountField('#customerAccountNumber', {
fieldType: 'customerAccountNumber',
label: 'Account Number',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
}
// else: mount the BASIC_CARD fields β see the examples below
});
sdk.onError((err) => console.error('SDK Error:', err));
sdk.init();
async function submitPayment(e) {
e.preventDefault();
try {
const result = await sdk.handleSubmit({
customerEmail: '[email protected]',
billingCountryCode: 'US'
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
console.error('SDK error on submit:', error);
}
}
Since a checkout session's
paymentMethodis fixed for the lifetime of that session, you only need to branch on it once, right afterinit()β there is no in-session switching betweenBASIC_CARDandBANKTRANSFERthe way there is between the saved-cards and new-card screens.
π Apple Pay & π Google Pay (via Evervault)
Apple Pay and Google Pay wallet payments are supported for BASIC_CARD checkout sessions. There is no separate APPLEPAY / GOOGLEPAY checkout paymentMethod β sdk.getCheckoutInfo().paymentMethod still reports 'BASIC_CARD'. Wallets are just an alternate way to call .handleSubmit() on that same session, alongside the card fields.
The Hosted Fields SDK only handles the submit side of a wallet payment. Collecting the wallet token is done with Evervault's own JavaScript SDK, which you load and initialize directly on your checkout page with your own Evervault account. This SDK does not load Evervault for you.
1. Load and initialize Evervault
<script src="https://js.evervault.com/v2" onload="onEvervaultReady()" async></script>
let evervault;
function onEvervaultReady() {
evervault = new Evervault('<EVERVAULT_TEAM_ID>', '<EVERVAULT_APP_ID>');
tryMountWallets();
}
β οΈ Load the script with
asyncand wait foronloadbefore calling any Evervault API β see Asynchronous Loading in Evervault's docs. Since this andsdk.onReady()resolve independently, only mount the wallet buttons once both have fired.
2. Create a transaction and mount the wallet buttons
Use sdk.getCheckoutInfo() for the checkout's amount and currency:
let sdkReady = false;
let evervaultReady = false;
sdk.onReady(() => {
sdkReady = true;
tryMountWallets();
});
function tryMountWallets() {
if (!sdkReady || !evervaultReady) return;
const { amount, currency } = sdk.getCheckoutInfo();
const transaction = evervault.transactions.create({
amount: Math.round(amount * 100), // see note below
currency,
country: 'US',
merchantId: '<EVERVAULT_MERCHANT_ID>'
});
mountApplePay(transaction);
mountGooglePay(transaction);
}
β οΈ Evervault expects
amountin the smallest currency unit (e.g. cents), the same convention as Stripe.sdk.getCheckoutInfo().amountis in major units (e.g.1for $1.00) β convert before callingevervault.transactions.create(), or the wallet sheet will show 100x less than the actual checkout total.
function mountApplePay(transaction) {
const apple = evervault.ui.applePay(transaction, {
process: async (paymentMethod, { fail }) => {
try {
const result = await sdk.handleSubmit({
paymentMethod: 'APPLEPAY',
applePay: paymentMethod
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
fail({ message: error?.message || 'Payment failed' });
}
}
});
apple.availability().then((availability) => {
if (availability === 'unsupported') return;
apple.mount('#apple-pay-button');
});
}
function mountGooglePay(transaction) {
const google = evervault.ui.googlePay(transaction, {
process: async (paymentMethod, { fail }) => {
try {
const result = await sdk.handleSubmit({
paymentMethod: 'GOOGLEPAY',
googlePay: paymentMethod
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
fail({ message: error?.message || 'Payment failed' });
}
}
});
google.mount('#google-pay-button');
}
3. Submit
Pass the object Evervault handed to process straight through to .handleSubmit() β see .handleSubmit() above for the exact parameters (paymentMethod, applePay, googlePay). No reshaping needed on your side; the SDK converts it into the format the payment gateway expects.
Apple Pay requirements
Apple Pay has platform-level requirements that apply regardless of this SDK:
- HTTPS is mandatory.
ApplePaySessionrefuses to start on an insecure origin. Unlike most secure-context browser APIs,localhostover plainhttp://is not treated as an exception β you need a real TLS certificate to test it, even locally. - Domain verification with Evervault. Your checkout domain must be verified for Apple Pay before the button becomes available. Either verify it through the Evervault Dashboard (Payments β Apple Pay), or serve Evervault's identifier file at
/.well-known/apple-developer-merchantid-domain-associationand register the domain via their Merchant API. Verification can take up to 10 minutes, and the identifier file must stay reachable afterwards β Apple periodically re-checks it and will silently disable the domain if it stops responding. -
Non-Safari browsers.
window.ApplePaySessiononly exists natively in Safari. To make the button available (and detectable) in other browsers, load Apple's own compatibility library whenever it's missing, before mounting the button:function ensureApplePaySDK() { if (window.ApplePaySession) return Promise.resolve(); return new Promise((resolve) => { const script = document.createElement('script'); script.src = 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js'; script.crossOrigin = 'anonymous'; script.addEventListener('load', () => resolve(), { once: true }); script.addEventListener('error', () => resolve(), { once: true }); document.head.appendChild(script); }); } -
Even with the polyfill loaded,
apple.availability()still resolves to'unsupported'unless the browser/device combination can actually pay β e.g. on macOS, the Mac needs Touch ID (or a paired iPhone/Apple Watch with Apple Pay set up) and a card added to Apple Wallet.
πΌ Full examples
Form
This example is for a checkout page without saved cards. For saved cards support see the next example. The HTML is intentionally minimal β add your own styles, icons, and layout as needed.
<div class="card">
<div class="card-header">
<h3>Basic card</h3>
<button id="themeToggle" onclick="toggleTheme()">Toggle theme</button>
</div>
<!-- shown when init(), a field, or handleSubmit() fails -->
<div id="form-error" style="display:none; color:#D7353B;"></div>
<!-- shown while the SDK loads, hidden once fields are mounted -->
<div id="loading-container"></div>
<form class="card-body" autocomplete="on" onsubmit="submitPayment(event)">
<div id="cardNumber"></div>
<div class="field-row">
<div id="expiryDate"></div>
<div id="cvv"></div>
</div>
<div id="cardholderName"></div>
<button type="submit">Submit payment</button>
</form>
</div>
<!-- Load the Hosted Fields SDK -->
<script src="https://your-domain.com/hf/sdk/lib/umd/index.js"></script>
<script>
const sdk = HostedFieldsSDK({ paymentId: '<payment_id>', theme: 'dark' });
// show a loading skeleton while the SDK fetches session data
sdk.renderLoadingSkeleton('#loading-container', 4);
sdk.onReady(() => {
loadCheckoutData();
});
function loadCheckoutData() {
document.getElementById('loading-container').style.display = 'none';
sdk.mountField('#cardNumber', {
fieldType: 'cardNumber',
label: 'Card Number',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
sdk.mountField('#expiryDate', {
fieldType: 'expiryDate',
label: 'Expiry Date',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
sdk.mountField('#cvv', {
fieldType: 'cvv',
label: 'Card Security Code',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
sdk.mountField('#cardholderName', {
fieldType: 'cardholderName',
label: 'Cardholder Name',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
}
// Toggle between light and dark themes
let isDark = true;
function toggleTheme() {
isDark = !isDark;
sdk.setTheme(isDark ? 'dark' : 'light');
}
function showFormError(message) {
const el = document.getElementById('form-error');
el.textContent = message;
el.style.display = 'block';
}
function clearFormError() {
document.getElementById('form-error').style.display = 'none';
}
sdk.onError((err) => {
console.error("SDK Error:", err);
showFormError(err.message);
});
sdk.onFieldValid((info) => console.log("Field valid:", info));
sdk.init();
async function submitPayment(e) {
e.preventDefault(); // prevent the form from reloading the page
clearFormError();
try {
const result = await sdk.handleSubmit({
customerEmail: '[email protected]',
billingCountryCode: 'US'
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
// already surfaced via onError β no need to duplicate
console.error('SDK error on submit:', error);
}
}
</script>
Saved cards + basic card
The HTML below is intentionally minimal β it focuses on SDK integration logic, not visual design. In a real implementation you would add your own button styles, icons, card brand logos, loading states, and layout as needed. Container
idvalues (e.g.#cvv,#cvv-container) can be any name you choose β just make sure the same selector is used in both your HTML and the correspondingmountFieldcall.
<button onclick="toggleTheme()">Toggle theme</button>
<!-- shown when init(), a field, or handleSubmit() fails -->
<div id="form-error" style="display:none; color:#D7353B;"></div>
<!-- Saved cards screen -->
<div id="saved-cards-screen">
<div id="saved-cards-list"></div>
<div id="cvv-container"></div>
<button onclick="showBasicCardForm()">+ Add new card</button>
<button onclick="paySavedCard()">Submit payment</button>
</div>
<!-- Basic card screen -->
<div id="basic-card-screen" style="display:none">
<button onclick="showSavedCards()">β Back</button>
<div id="cardNumber"></div>
<div class="field-row">
<div id="expiryDate"></div>
<div id="cvv"></div>
</div>
<div id="cardholderName"></div>
<label><input type="checkbox" id="saveCard"> Save card for future payments</label>
<button onclick="payBasicCard()">Submit payment</button>
</div>
<!-- Delete confirmation modal -->
<div id="delete-modal" style="display:none">
<div>
<p>Delete <span id="delete-modal-card"></span>?</p>
<button onclick="closeDeleteModal()">Cancel</button>
<button id="confirm-delete">Delete</button>
</div>
</div>
<script src="https://your-domain.com/hf/sdk/lib/umd/index.js"></script>
<script>
const sdk = HostedFieldsSDK({ paymentId: '<payment_id>', theme: 'dark' });
let selectedCardId = null;
let isDark = true;
function toggleTheme() {
isDark = !isDark;
sdk.setTheme(isDark ? 'dark' : 'light');
}
const fieldStyles = {
variant: 'outlined',
labelPosition: 'floating',
};
sdk.onReady(() => {
loadCheckoutData();
});
function loadCheckoutData() {
const cards = sdk.getSavedCards();
if (cards.length > 0) {
showSavedCards();
} else {
showBasicCardForm();
}
}
function showFormError(message) {
const el = document.getElementById('form-error');
el.textContent = message;
el.style.display = 'block';
}
function clearFormError() {
document.getElementById('form-error').style.display = 'none';
}
sdk.onError((err) => {
console.error('SDK Error:', err);
showFormError(err.message);
});
sdk.init();
// ββ Saved cards screen ββββββββββββββββββββββββββββββββββββββββββ
function showSavedCards() {
clearFormError();
sdk.unmountFields(['cardNumber', 'expiryDate', 'cvv', 'cardholderName']);
document.getElementById('saved-cards-screen').style.display = 'block';
document.getElementById('basic-card-screen').style.display = 'none';
renderCardList(sdk.getSavedCards());
}
function renderCardList(cards) {
const list = document.getElementById('saved-cards-list');
list.innerHTML = '';
cards.forEach(card => {
const item = document.createElement('div');
item.dataset.cardId = card.id;
item.innerHTML = `
<span>${card.brand.toUpperCase()}</span>
<span>${card.panMasked}</span>
<span>${card.expiryMonth}/${card.expiryYear}</span>
<button onclick="selectCard(${card.id}, '${card.brand}')">Select</button>
<button onclick="deleteCard(${card.id})">Delete</button>
`;
list.appendChild(item);
});
}
function selectCard(cardId, brand) {
selectedCardId = cardId;
// highlight selected card
document.querySelectorAll('#saved-cards-list > div').forEach(el => {
el.style.outline = el.dataset.cardId === String(cardId) ? '1px solid #512ccc' : 'none';
});
// Always unmount before mounting β clears any CVV entered for a previously
// selected card. Without this the old value stays if the user switches cards.
sdk.unmountField('cvv');
sdk.mountField('#cvv-container', {
fieldType: 'cvv',
label: 'Card Security Code',
fieldStyles,
});
// inform the CVV field of the card brand (affects required digit count)
sdk.setCardBrand(brand);
}
// Opens a custom confirmation modal β the SDK has no built-in one.
// You can also call sdk.deleteCard() directly without confirmation if you prefer.
function deleteCard(cardId) {
const card = sdk.getSavedCards().find(c => c.id === cardId);
if (!card) return;
document.getElementById('delete-modal-card').textContent =
`${card.brand.toUpperCase()} β’β’β’β’ ${card.panMasked.slice(-4)}`;
document.getElementById('delete-modal').style.display = 'block';
document.getElementById('confirm-delete').onclick = async () => {
closeDeleteModal();
await sdk.deleteCard(cardId);
if (selectedCardId === cardId) {
sdk.unmountField('cvv');
selectedCardId = null;
}
const updatedCards = sdk.getSavedCards();
if (updatedCards.length === 0) showBasicCardForm();
else renderCardList(updatedCards);
};
}
function closeDeleteModal() {
document.getElementById('delete-modal').style.display = 'none';
}
async function paySavedCard() {
if (!selectedCardId) return showFormError('Please select a card.');
clearFormError();
try {
const result = await sdk.handleSubmit({
selectedCardId,
billingCountryCode: 'US',
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
// already surfaced via onError β no need to duplicate
console.error('SDK error on submit:', error);
}
}
// ββ Basic card screen βββββββββββββββββββββββββββββββββββββββββββ
function showBasicCardForm() {
clearFormError();
sdk.unmountField('cvv'); // remove CVV from saved card container
document.getElementById('saved-cards-screen').style.display = 'none';
document.getElementById('basic-card-screen').style.display = 'block';
sdk.mountField('#cardNumber', { fieldType: 'cardNumber', label: 'Card Number', fieldStyles });
sdk.mountField('#expiryDate', { fieldType: 'expiryDate', label: 'Expiry Date', fieldStyles });
sdk.mountField('#cvv', { fieldType: 'cvv', label: 'Card Security Code', fieldStyles });
sdk.mountField('#cardholderName', { fieldType: 'cardholderName', label: 'Cardholder Name', fieldStyles });
}
async function payBasicCard() {
clearFormError();
try {
const result = await sdk.handleSubmit({
saveCard: document.getElementById('saveCard').checked ? 'true' : 'false',
billingCountryCode: 'US',
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
// already surfaced via onError β no need to duplicate
console.error('SDK error on submit:', error);
}
}
</script>
Bank transfer
This example is for a checkout page whose
paymentMethodisBANKTRANSFERβ it mounts onlycustomerAccountNumber, none of theBASIC_CARDfields. The HTML is intentionally minimal β add your own styles, icons, and layout as needed.
<div class="card">
<div class="card-header">
<h3>Bank transfer</h3>
<button id="themeToggle" onclick="toggleTheme()">Toggle theme</button>
</div>
<!-- shown when init(), the field, or handleSubmit() fails -->
<div id="form-error" style="display:none; color:#D7353B;"></div>
<!-- shown while the SDK loads, hidden once the field is mounted -->
<div id="loading-container"></div>
<form class="card-body" autocomplete="on" onsubmit="submitPayment(event)">
<div id="customerAccountNumber"></div>
<button type="submit">Submit payment</button>
</form>
</div>
<!-- Load the Hosted Fields SDK -->
<script src="https://your-domain.com/hf/sdk/lib/umd/index.js"></script>
<script>
const sdk = HostedFieldsSDK({ paymentId: '<payment_id>', theme: 'dark' });
// show a loading skeleton while the SDK fetches session data
sdk.renderLoadingSkeleton('#loading-container', 1);
sdk.onReady(() => {
loadCheckoutData();
});
function loadCheckoutData() {
document.getElementById('loading-container').style.display = 'none';
sdk.mountField('#customerAccountNumber', {
fieldType: 'customerAccountNumber',
label: 'Account Number',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
}
// Toggle between light and dark themes
let isDark = true;
function toggleTheme() {
isDark = !isDark;
sdk.setTheme(isDark ? 'dark' : 'light');
}
function showFormError(message) {
const el = document.getElementById('form-error');
el.textContent = message;
el.style.display = 'block';
}
function clearFormError() {
document.getElementById('form-error').style.display = 'none';
}
sdk.onError((err) => {
console.error('SDK Error:', err);
showFormError(err.message);
});
sdk.onFieldValid((info) => console.log('Field valid:', info));
sdk.init();
async function submitPayment(e) {
e.preventDefault(); // prevent the form from reloading the page
clearFormError();
try {
const result = await sdk.handleSubmit({
customerEmail: '[email protected]',
billingCountryCode: 'US'
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
// already surfaced via onError β no need to duplicate
console.error('SDK error on submit:', error);
}
}
</script>
If your integration supports both
BASIC_CARDandBANKTRANSFERon the same page, don't hardcode which fields to mount β checksdk.getCheckoutInfo().paymentMethodinsideloadCheckoutData()(right after.init()resolves) and branch to the right fields, reusing the samesdkinstance,#form-errorelement, andtoggleTheme()logic shown here:function loadCheckoutData() { document.getElementById('loading-container').style.display = 'none'; const { paymentMethod } = sdk.getCheckoutInfo(); if (paymentMethod === 'BANKTRANSFER') { sdk.mountField('#customerAccountNumber', { fieldType: 'customerAccountNumber', label: 'Account Number', fieldStyles: { variant: 'outlined', labelPosition: 'floating' } }); } else { // BASIC_CARD β mount the four card fields instead, see the examples above } }A single checkout session's
paymentMethodnever changes after.init()resolves, so this check only needs to run once.
Apple Pay & Google Pay
This example is for a
BASIC_CARDcheckout page that also offers Apple Pay / Google Pay via Evervault β see Apple Pay & Google Pay (via Evervault) above for the full walkthrough. Replace<EVERVAULT_TEAM_ID>,<EVERVAULT_APP_ID>, and<EVERVAULT_MERCHANT_ID>with your own Evervault credentials. The HTML is intentionally minimal β add your own styles, icons, and layout as needed.
<div class="card">
<div class="card-header">
<h3>Basic card</h3>
<button id="themeToggle" onclick="toggleTheme()">Toggle theme</button>
</div>
<!-- shown when init(), a field, or handleSubmit() fails -->
<div id="form-error" style="display:none; color:#D7353B;"></div>
<!-- shown while the SDK loads, hidden once fields are mounted -->
<div id="loading-container"></div>
<div class="card-body">
<!-- hidden until Evervault confirms the wallet is actually available -->
<div id="apple-pay-button" style="display:none; height: 48px;"></div>
<div id="google-pay-button" style="display:none; height: 48px;"></div>
<form autocomplete="on" onsubmit="submitPayment(event)">
<div id="cardNumber"></div>
<div class="field-row">
<div id="expiryDate"></div>
<div id="cvv"></div>
</div>
<div id="cardholderName"></div>
<button type="submit">Submit payment</button>
</form>
</div>
</div>
<!-- Load the Hosted Fields SDK -->
<script src="https://your-domain.com/hf/sdk/lib/umd/index.js"></script>
<!-- Load Evervault asynchronously β onEvervaultReady() is defined below -->
<script src="https://js.evervault.com/v2" onload="onEvervaultReady()" async></script>
<script>
const sdk = HostedFieldsSDK({ paymentId: '<payment_id>', theme: 'dark' });
// show a loading skeleton while the SDK fetches session data
sdk.renderLoadingSkeleton('#loading-container', 4);
let evervault;
let sdkReady = false;
let evervaultReady = false;
sdk.onReady(() => {
sdkReady = true;
loadCheckoutData();
tryMountWallets();
});
// called by the <script onload> above once Evervault has loaded
function onEvervaultReady() {
evervault = new Evervault('<EVERVAULT_TEAM_ID>', '<EVERVAULT_APP_ID>');
evervaultReady = true;
tryMountWallets();
}
function loadCheckoutData() {
document.getElementById('loading-container').style.display = 'none';
sdk.mountField('#cardNumber', {
fieldType: 'cardNumber',
label: 'Card Number',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
sdk.mountField('#expiryDate', {
fieldType: 'expiryDate',
label: 'Expiry Date',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
sdk.mountField('#cvv', {
fieldType: 'cvv',
label: 'Card Security Code',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
sdk.mountField('#cardholderName', {
fieldType: 'cardholderName',
label: 'Cardholder Name',
fieldStyles: { variant: 'outlined', labelPosition: 'floating' }
});
}
// sdk.onReady() and Evervault's onload fire independently β only
// mount the wallet buttons once both are ready.
function tryMountWallets() {
if (!sdkReady || !evervaultReady) return;
const { amount, currency } = sdk.getCheckoutInfo();
// Evervault expects amount in the smallest currency unit (e.g. cents)
const transaction = evervault.transactions.create({
amount: Math.round(amount * 100),
currency,
country: 'US',
merchantId: '<EVERVAULT_MERCHANT_ID>'
});
mountApplePay(transaction);
mountGooglePay(transaction);
}
function mountApplePay(transaction) {
const apple = evervault.ui.applePay(transaction, {
process: async (paymentMethod, { fail }) => {
await submitWallet('APPLEPAY', 'applePay', paymentMethod, fail);
}
});
apple.availability().then((availability) => {
if (availability === 'unsupported') return;
document.getElementById('apple-pay-button').style.display = 'block';
apple.mount('#apple-pay-button');
});
}
function mountGooglePay(transaction) {
const google = evervault.ui.googlePay(transaction, {
process: async (paymentMethod, { fail }) => {
await submitWallet('GOOGLEPAY', 'googlePay', paymentMethod, fail);
}
});
document.getElementById('google-pay-button').style.display = 'block';
google.mount('#google-pay-button');
}
// shared submit logic for both wallets β pass the token straight through
async function submitWallet(paymentMethod, key, token, fail) {
clearFormError();
try {
const result = await sdk.handleSubmit({ paymentMethod, [key]: token });
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
const message = error?.message || 'Payment failed. Please try again.';
showFormError(message);
fail({ message });
}
}
// Toggle between light and dark themes
let isDark = true;
function toggleTheme() {
isDark = !isDark;
sdk.setTheme(isDark ? 'dark' : 'light');
}
function showFormError(message) {
const el = document.getElementById('form-error');
el.textContent = message;
el.style.display = 'block';
}
function clearFormError() {
document.getElementById('form-error').style.display = 'none';
}
sdk.onError((err) => {
console.error('SDK Error:', err);
showFormError(err.message);
});
sdk.init();
async function submitPayment(e) {
e.preventDefault(); // prevent the form from reloading the page
clearFormError();
try {
const result = await sdk.handleSubmit({
customerEmail: '[email protected]',
billingCountryCode: 'US'
});
if (result?.redirectUrl) window.location.replace(result.redirectUrl);
} catch (error) {
// already surfaced via onError β no need to duplicate
console.error('SDK error on submit:', error);
}
}
</script>
On a non-Safari browser,
#apple-pay-buttonstays hidden unless you also load Apple's compatibility polyfill beforetryMountWallets()runs β see Non-Safari browsers above.