Plain HTML and JavaScript
Add highwire.js to an existing plain HTML checkout with a self-hosted UMD build.
Before you begin
Use this guide when an existing browser checkout uses a plain HTML form and loads JavaScript with a <script> tag. Complete the artifact placement and integrity setup in Getting started first.
Your checkout remains responsible for its inputs, validation, and payment request. This guide uses buildExistingPaymentRequest(form) to represent the existing merchant-owned function that validates the form and returns the complete request body already sent to your backend. Keep that function and its payment fields outside the highwire.js integration.
Complete example
The inputs below provide stable selectors for paste-event observation. highwire.js observes only whether a paste occurs; it never reads their values.
<form id="checkout">
<label>
Card number
<input id="card-number" autocomplete="cc-number" required>
</label>
<label>
Expiration
<input id="card-exp" autocomplete="cc-exp" required>
</label>
<label>
Security code
<input id="card-cvc" autocomplete="cc-csc" required>
</label>
<label>
Billing postal code
<input id="billing-zip" autocomplete="postal-code" required>
</label>
<button type="submit">Pay</button>
</form>
<script
src="/assets/highwire-v<version>.js"
integrity="sha384-REPLACE_WITH_THE_HASH_HIGHWIRE_SENT_YOU"
crossorigin="anonymous"
defer
></script>
<script src="/assets/checkout-highwire.js" defer></script>Place the integration code in /assets/checkout-highwire.js on the same origin:
const form = document.getElementById('checkout');
if (!(form instanceof HTMLFormElement)) {
throw new Error('Checkout form not found');
}
const hw = HighWire();
const observedFields = [
['#card-number', 'number'],
['#card-cvc', 'cvc'],
['#card-exp', 'exp'],
['#billing-zip', 'zip'],
];
observedFields.forEach(([selector, fieldName]) => {
const element = form.querySelector(selector);
if (!element) throw new Error(`Missing observed field: ${selector}`);
hw.observeField(element, fieldName);
});
async function collectDeviceDataOrNull() {
try {
return await hw.collect();
} catch {
return null;
}
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
const existingPaymentRequest = buildExistingPaymentRequest(form);
const deviceData = await collectDeviceDataOrNull();
const response = await fetch('/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...existingPaymentRequest,
...(deviceData ? { deviceData } : {}),
}),
});
if (!response.ok) {
throw new Error('Payment request failed');
}
});The example intentionally does not define buildExistingPaymentRequest(form). Replace that call with the existing function your checkout uses to produce its complete, validated payment request. The only property this integration adds is optional deviceData.
If your form uses different selectors, change only the selectors in observedFields. Keep each field name passed to observeField() in the supported wire format shown in the API reference.
When collection is unavailable
Handled provider or required-context failures resolve to null; missing or broken platform primitives can reject. The helper converts either unavailable outcome to null, so the checkout can continue without deviceData. Do not send null, an empty object, or a hand-built substitute.
The example throws when the merchant endpoint returns a non-success response. Route that condition through the checkout's existing error handling. For collection and script-loading failures, follow Troubleshooting.