Collect data entered by customers and validate it.
By validating data prior to transaction initiation, you can:
- Reduce the risk of fraudulent payments.
- Offer a better user experience, by providing clear feedback that helps customers complete forms correctly.
- Ensure you meet PCI and regulatory requirements.
There are three key validation methods:
getValueAsync(): Retrieves current field values asynchronously.handleValidation(): Validates field data and returns results.validate(): Validates component data (for complex components).
Valication can happen:
- Automatically on user input (
validationOnChange,validationOnBlur). - When
submitAsync()is called. - When you call validation methods directly.
The following table shows the compatibility of these methods with the different components.
| Component name | getValueAsync() | handleValidation() | validate() |
|---|---|---|---|
| Address | |||
| Billing address | |||
| Card brand selector | |||
| Card consent | |||
| Card CVC | |||
| Card expiry date | |||
| Cardholder name | |||
| Card number | |||
| Card-on-file | |||
| Card submit | |||
| Click-once | |||
| Country selection | |||
| Dynamic card image | |||
| New card | |||
| Postcode | |||
| Pre-fill billing address checkbox |
Use getValueAsync() to collect form data before processing payments.
async function processPayment() {
// Get individual card field values
const cardNumber = await cardNumberComponent.getValueAsync();
const expiryDate = await expiryDateComponent.getValueAsync();
const cvc = await cvcComponent.getValueAsync();
const holderName = await holderNameComponent.getValueAsync();
// Get complex component values
const addressData = await billingAddressComponent.getValueAsync();
console.log('Payment data collected:', {
cardNumber: cardNumber ? '****' + cardNumber.slice(-4) : null,
expiryDate,
addressData
});
// Submit payment with collected data
const paymentResult = await cardSubmitComponent.submitAsync();
return paymentResult;
}Check specific fields manually when needed.
async function validateCardNumber() {
// Get current card number value
const cardNumber = await cardNumberComponent.getValueAsync();
// Check if field has a value
if (!cardNumber) {
showFieldError('cardNumber', 'Card number is required');
return false;
}
// Run validation
const validationResults = await cardNumberComponent.handleValidation();
// Check if validation passed
const isValid = validationResults.every(result => result.valid);
if (!isValid) {
console.log('Card number validation failed:', validationResults);
showFieldError('cardNumber', 'Please enter a valid card number');
return false;
}
// Clear any existing errors
clearFieldError('cardNumber');
return true;
}
async function validateExpiryDate() {
const expiryDate = await expiryDateComponent.getValueAsync();
if (!expiryDate) {
showFieldError('expiryDate', 'Expiry date is required');
return false;
}
const validationResults = await expiryDateComponent.handleValidation();
const isValid = validationResults.every(result => result.valid);
if (!isValid) {
// Get specific error message
const error = validationResults.find(result => !result.valid);
const errorMessage = error?.errors ? Object.values(error.errors)[0]?.message : 'Invalid expiry date';
showFieldError('expiryDate', errorMessage);
return false;
}
clearFieldError('expiryDate');
return true;
}Ensure all required fields are valid before allowing payment submission.
async function handleFormSubmit() {
console.log('Starting form validation...');
// Step 1: Check if required fields have values
const cardNumber = await cardNumberComponent.getValueAsync();
const expiryDate = await expiryDateComponent.getValueAsync();
const cvc = await cvcComponent.getValueAsync();
const missingFields = [];
if (!cardNumber) missingFields.push('Card number');
if (!expiryDate) missingFields.push('Expiry date');
if (!cvc) missingFields.push('Security code');
if (missingFields.length > 0) {
alert(`Please complete these required fields: ${missingFields.join(', ')}`);
return false;
}
// Step 2: Validate all fields
try {
const validationPromises = [
cardNumberComponent.handleValidation(),
expiryDateComponent.handleValidation(),
cvcComponent.handleValidation()
];
// Add cardholder name if required
if (holderNameComponent) {
validationPromises.push(holderNameComponent.handleValidation());
}
// Add billing address if configured
if (billingAddressComponent) {
const addressData = await billingAddressComponent.getValueAsync();
validationPromises.push(billingAddressComponent.validate(addressData));
}
const allValidationResults = await Promise.all(validationPromises);
const flatResults = allValidationResults.flat();
// Check if all validations passed
const allValid = flatResults.every(result => result.valid);
if (allValid) {
console.log('All validations passed, processing payment...');
await processPayment();
return true;
} else {
console.log('Validation failed:', flatResults.filter(result => !result.valid));
alert('Please fix the errors in your payment details');
// Show specific field errors
displayValidationErrors(flatResults);
return false;
}
} catch (error) {
console.error('Validation error:', error);
alert('Unable to validate payment details. Please try again.');
return false;
}
}
function displayValidationErrors(validationResults) {
validationResults.forEach(result => {
if (!result.valid && result.errors) {
Object.entries(result.errors).forEach(([fieldName, error]) => {
showFieldError(fieldName, error.message);
});
}
});
}Configure components to validate as users type or when they leave fields.
// Card number with real-time validation
const cardNumberComponent = new CardNumberComponent(sdkConfig, {
validationOnChange: true, // Validate as user types
validationOnBlur: true, // Validate when user leaves field
onValidation: (results) => {
const isValid = results.every(result => result.valid);
if (!isValid) {
// Show error immediately
const error = results.find(result => !result.valid);
const errorMessage = getFirstErrorMessage(error);
showFieldError('cardNumber', errorMessage);
// Disable submit button
disableSubmitButton();
} else {
// Clear error and potentially enable submit
clearFieldError('cardNumber');
checkIfFormValid();
}
}
});
// CVC with immediate feedback
const cvcComponent = new CardCvcComponent(sdkConfig, {
validationOnChange: true,
onValidation: (results) => {
const isValid = results.every(result => result.valid);
// Update visual indicator
const cvcField = document.getElementById('cvc-field');
cvcField.classList.toggle('is-valid', isValid);
cvcField.classList.toggle('is-invalid', !isValid);
if (!isValid) {
showFieldError('cvc', 'Please enter a valid security code');
} else {
clearFieldError('cvc');
}
}
});
function getFirstErrorMessage(validationResult) {
if (validationResult.errors) {
const firstError = Object.values(validationResult.errors)[0];
return firstError?.message || 'Invalid input';
}
return 'Invalid input';
}
function checkIfFormValid() {
// Enable submit button only if all fields are valid
const allFieldsValid = document.querySelectorAll('.is-invalid').length === 0;
const submitButton = document.getElementById('submit-button');
submitButton.disabled = !allFieldsValid;
}Validate different fields depending on the payment method selected.
async function validateBasedOnPaymentType() {
const paymentType = getSelectedPaymentType();
console.log('Validating for payment type:', paymentType);
if (paymentType === 'new-card') {
// Validate all card fields for new card
return await validateAllCardFields();
} else if (paymentType === 'saved-card') {
// Only validate CVC for saved cards
const cvcValidation = await cvcComponent.handleValidation();
const isValid = cvcValidation.every(result => result.valid);
if (!isValid) {
showFieldError('cvc', 'Please enter the security code for your saved card');
}
return isValid;
} else if (paymentType === 'paypal') {
// No card validation needed for PayPal
return true;
} else if (paymentType === 'apple-pay') {
// Apple Pay handles its own validation
return await validateApplePayAvailability();
}
return false;
}
async function validateAllCardFields() {
const validations = await Promise.all([
cardNumberComponent.handleValidation(),
expiryDateComponent.handleValidation(),
cvcComponent.handleValidation(),
holderNameComponent.handleValidation()
]);
const allValid = validations.flat().every(result => result.valid);
if (!allValid) {
showError('Please complete all card details correctly');
}
return allValid;
}
function getSelectedPaymentType() {
const selectedRadio = document.querySelector('input[name="payment-type"]:checked');
return selectedRadio?.value || 'new-card';
}Apply country-specific validation rules for billing addresses.
async function validateAddressByCountry() {
const addressData = await billingAddressComponent.getValueAsync();
if (!addressData.countryCode) {
showError('Please select a country');
return false;
}
console.log('Validating address for country:', addressData.countryCode);
// Country-specific validation rules
switch (addressData.countryCode) {
case 'US':
return validateUSAddress(addressData);
case 'GB':
return validateUKAddress(addressData);
case 'CA':
return validateCanadianAddress(addressData);
case 'DE':
return validateGermanAddress(addressData);
default:
return validateGenericAddress(addressData);
}
}
function validateUSAddress(addressData) {
// US: Require ZIP code in 5 or 9 digit format
const zipRegex = /^\d{5}(-\d{4})?$/;
if (!addressData.postalCode) {
showFieldError('postalCode', 'ZIP code is required');
return false;
}
if (!zipRegex.test(addressData.postalCode)) {
showFieldError('postalCode', 'Please enter a valid US ZIP code (e.g., 12345 or 12345-6789)');
return false;
}
// Require street address
if (!addressData.houseNumberOrName || addressData.houseNumberOrName.trim().length < 5) {
showFieldError('address', 'Please enter a complete street address');
return false;
}
clearFieldError('postalCode');
clearFieldError('address');
return true;
}
function validateUKAddress(addressData) {
// UK: Validate postcode format
const postcodeRegex = /^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$/i;
if (!addressData.postalCode) {
showFieldError('postalCode', 'Postcode is required');
return false;
}
if (!postcodeRegex.test(addressData.postalCode)) {
showFieldError('postalCode', 'Please enter a valid UK postcode (e.g., SW1A 1AA)');
return false;
}
clearFieldError('postalCode');
return true;
}
function validateCanadianAddress(addressData) {
// Canadian postal code: A1A 1A1 format
const postalCodeRegex = /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i;
if (!addressData.postalCode) {
showFieldError('postalCode', 'Postal code is required');
return false;
}
if (!postalCodeRegex.test(addressData.postalCode)) {
showFieldError('postalCode', 'Please enter a valid Canadian postal code (e.g., K1A 0A6)');
return false;
}
clearFieldError('postalCode');
return true;
}
function validateGenericAddress(addressData) {
// Basic validation for other countries
if (!addressData.houseNumberOrName) {
showFieldError('address', 'Address is required');
return false;
}
if (!addressData.postalCode) {
showFieldError('postalCode', 'Postal code is required');
return false;
}
clearFieldError('address');
clearFieldError('postalCode');
return true;
}Use the onCustomValidation callback to validate your merchant-owned fields (shipping address, email, terms acceptance) alongside SDK components (billing address) on the first submit attempt. This eliminates the two-phase validation flow where customers see merchant errors first, then SDK errors after a second submission.
Without onCustomValidation, validation happens in two phases:
- Customer clicks submit
- Merchant validates own fields (shipping, email, etc.)
- If merchant validation fails → errors shown, process stops
- If merchant validation passes → SDK validates billing address
- If SDK validation fails → errors shown, customer must submit again
This creates a poor user experience where customers believe the form is complete after fixing the first set of errors, only to discover hidden errors on the second attempt.
With onCustomValidation, all validation happens at once:
- Customer clicks submit
- Merchant and SDK validation run simultaneously
- All errors displayed together on first attempt
- Customer fixes all issues in one go
- Form submits successfully
// Create billing address component
const billingAddress = sdk.create('billing-address', {
required: true
});
billingAddress.mount('billing-address-container');
// Create card submit with onCustomValidation
const cardSubmit = sdk.create('card-submit', {
submitText: 'Complete purchase',
disableUntilValidated: true,
avsRequest: true,
cardNumberComponent: cardNumber,
cardExpiryDateComponent: cardExpiry,
cardCvcComponent: cardCvc,
billingAddressComponents: {
billingAddressComponent: billingAddress
},
// Validate all merchant fields alongside SDK billing address
onCustomValidation: async () => {
let allValid = true;
// Clear previous errors
clearAllErrors();
// Validate shipping address
const shippingAddress = document.getElementById('shipping-address').value;
if (!shippingAddress || shippingAddress.trim().length < 5) {
showFieldError('shipping-address', 'Please enter a complete shipping address');
allValid = false;
}
// Validate email
const email = document.getElementById('email').value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
showFieldError('email', 'Please enter a valid email address');
allValid = false;
}
// Validate phone number
const phone = document.getElementById('phone').value;
const phoneRegex = /^\+?[\d\s\-()]{10,}$/;
if (!phone || !phoneRegex.test(phone)) {
showFieldError('phone', 'Please enter a valid phone number');
allValid = false;
}
// Validate terms acceptance
const termsAccepted = document.getElementById('terms-checkbox').checked;
if (!termsAccepted) {
showFieldError('terms', 'You must accept the terms and conditions to proceed');
allValid = false;
}
// Validate privacy policy acceptance
const privacyAccepted = document.getElementById('privacy-checkbox').checked;
if (!privacyAccepted) {
showFieldError('privacy', 'You must accept the privacy policy to proceed');
allValid = false;
}
// Return false to prevent submission if any validation failed
// SDK will still validate billing address and display those errors too
return allValid;
},
onPreAuthorisation: async (data) => {
// Proceed with transaction
return {
psd2Data: {}
};
},
onPostAuthorisation: (result) => {
console.log('Payment completed:', result.merchantTransactionId);
window.location.href = '/success';
},
onSubmitError: (error) => {
console.error('Payment failed:', error.message);
showErrorMessage('Payment failed. Please check your details and try again.');
}
});
// Helper functions for error display
function showFieldError(fieldId, message) {
const errorElement = document.getElementById(`${fieldId}-error`);
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
const fieldElement = document.getElementById(fieldId);
if (fieldElement) {
fieldElement.classList.add('field-error');
}
}
function clearAllErrors() {
document.querySelectorAll('.error-message').forEach(el => {
el.textContent = '';
el.style.display = 'none';
});
document.querySelectorAll('.field-error').forEach(el => {
el.classList.remove('field-error');
});
}
function showErrorMessage(message) {
const errorContainer = document.getElementById('general-error');
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
}The onCustomValidation callback works with all card integration modes:
const cardSubmit = sdk.create('card-submit', {
cardNumberComponent: cardNumber,
cardExpiryDateComponent: cardExpiry,
cardCvcComponent: cardCvc,
billingAddressComponents: {
billingAddressComponent: billingAddress
},
avsRequest: true,
onCustomValidation: async () => {
// Validate merchant fields for new card flow
return await validateShippingAndContactInfo();
}
});const clickOnce = sdk.create('click-once', {
isCvcRequired: true,
cardSubmitComponentConfig: {
billingAddressComponents: {
billingAddressComponent: billingAddress
},
avsRequest: true,
onCustomValidation: async () => {
// Validate merchant fields for click-once flow
return await validateShippingAndContactInfo();
}
}
});const cardOnFile = sdk.create('card-on-file', {
isCvcRequired: true
});
const cardSubmit = sdk.create('card-submit', {
useCardOnFile: true,
cardOnFileComponent: cardOnFile,
billingAddressComponents: {
billingAddressComponent: billingAddress
},
avsRequest: true,
onCustomValidation: async () => {
// Validate merchant fields for card-on-file flow
return await validateShippingAndContactInfo();
}
});When using onCustomValidation, follow these best practices:
- Always return a boolean: Return
trueto proceed,falseto prevent submission - Display all errors at once: Show validation errors for all invalid fields simultaneously
- Clear previous errors: Remove old error messages before re-validating
- Use async validation: Return a Promise if you need to perform async operations (like API calls)
- Provide clear error messages: Help users understand exactly what they need to fix
- Visual feedback: Use CSS classes to highlight invalid fields
- Accessibility: Ensure error messages are accessible to screen readers
onCustomValidation: async () => {
// Clear all previous errors first
clearAllErrors();
// Track validation state
const validationResults = {
shipping: false,
email: false,
terms: false
};
// Validate shipping address
const shippingAddress = getShippingAddress();
if (await validateAddressWithAPI(shippingAddress)) {
validationResults.shipping = true;
} else {
showFieldError('shipping-address', 'Invalid shipping address. Please verify and try again.');
}
// Validate email
const email = getEmail();
if (isValidEmail(email)) {
validationResults.email = true;
} else {
showFieldError('email', 'Please enter a valid email address (e.g., user@example.com).');
}
// Validate terms
if (isTermsAccepted()) {
validationResults.terms = true;
} else {
showFieldError('terms', 'Please read and accept the terms and conditions.');
// Set focus to terms checkbox for accessibility
document.getElementById('terms-checkbox')?.focus();
}
// Return overall validation result
return validationResults.shipping && validationResults.email && validationResults.terms;
}Implement your own validation rules based on business requirements.
async function validateBusinessRules() {
const cardNumber = await cardNumberComponent.getValueAsync();
const amount = getCurrentTransactionAmount();
const addressData = await billingAddressComponent.getValueAsync();
// Rule 1: Block certain card types for high-value transactions
if (amount > 1000) {
const cardType = detectCardType(cardNumber);
const blockedCards = ['DISCOVER', 'DINERS'];
if (blockedCards.includes(cardType)) {
showError(`${cardType} cards are not accepted for transactions over $1000`);
return false;
}
}
// Rule 2: Require address verification for international cards
const cardCountry = detectCardIssuingCountry(cardNumber);
const billingCountry = addressData.countryCode;
if (cardCountry !== billingCountry) {
const isAddressVerified = await performAddressVerification(addressData);
if (!isAddressVerified) {
showError('Address verification required for international cards');
return false;
}
}
// Rule 3: Velocity check - limit transactions per day
const dailyTransactionCount = await getDailyTransactionCount();
if (dailyTransactionCount >= 5) {
showError('Daily transaction limit reached. Please try again tomorrow.');
return false;
}
// Rule 4: Amount validation
if (amount < 1) {
showError('Transaction amount must be at least $1.00');
return false;
}
if (amount > 10000) {
showError('Transaction amount cannot exceed $10,000. Please contact support for larger transactions.');
return false;
}
return true;
}
function detectCardType(cardNumber) {
if (!cardNumber) return 'UNKNOWN';
const patterns = {
'VISA': /^4/,
'MASTERCARD': /^5[1-5]/,
'AMEX': /^3[47]/,
'DISCOVER': /^6(?:011|5)/,
'DINERS': /^3[0689]/
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(cardNumber)) {
return type;
}
}
return 'UNKNOWN';
}
async function performAddressVerification(addressData) {
try {
// This would integrate with your address verification service
const response = await fetch('/api/verify-address', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(addressData)
});
const result = await response.json();
return result.verified === true;
} catch (error) {
console.error('Address verification failed:', error);
// Fail open - allow transaction if verification service is down
return true;
}
}
function getCurrentTransactionAmount() {
// Get amount from your application state
return parseFloat(document.getElementById('amount-input')?.value || '0');
}