# Data validation

Collect data entered by customers and validate it.

## Overview

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.


## Validation methods

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


## Common validation scenarios

### Get values before payment processing

Use `getValueAsync()` to collect form data before processing payments.


```typescript
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;
}
```

### Validate individual fields

Check specific fields manually when needed.


```typescript
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;
}
```

### Validate all fields before submission

Ensure all required fields are valid before allowing payment submission.


```typescript
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);
      });
    }
  });
}
```

### Validate fields on user input

Configure components to validate as users type or when they leave fields.


```typescript
// 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 based on payment method

Validate different fields depending on the payment method selected.


```typescript
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';
}
```

### Validate billing addresses for different countries

Apply country-specific validation rules for billing addresses.


```typescript
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;
}
```

### Validate merchant and SDK fields simultaneously

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.

#### The problem with sequential validation

Without `onCustomValidation`, validation happens in two phases:

1. Customer clicks submit
2. Merchant validates own fields (shipping, email, etc.)
3. If merchant validation fails → errors shown, process stops
4. If merchant validation passes → SDK validates billing address
5. 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.

#### The solution: simultaneous validation

With `onCustomValidation`, all validation happens at once:

1. Customer clicks submit
2. Merchant and SDK validation run simultaneously
3. All errors displayed together on first attempt
4. Customer fixes all issues in one go
5. Form submits successfully



```typescript
// 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';
  }
}
```

#### Validation with different card component modes

The `onCustomValidation` callback works with all card integration modes:

##### New card with billing address


```typescript
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();
  }
});
```

##### Click-once (card on file with CVC re-entry)


```typescript
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();
    }
  }
});
```

##### Card on file (saved cards)


```typescript
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();
  }
});
```

#### Best practices

When using `onCustomValidation`, follow these best practices:

1. **Always return a boolean**: Return `true` to proceed, `false` to prevent submission
2. **Display all errors at once**: Show validation errors for all invalid fields simultaneously
3. **Clear previous errors**: Remove old error messages before re-validating
4. **Use async validation**: Return a Promise if you need to perform async operations (like API calls)
5. **Provide clear error messages**: Help users understand exactly what they need to fix
6. **Visual feedback**: Use CSS classes to highlight invalid fields
7. **Accessibility**: Ensure error messages are accessible to screen readers



```typescript
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;
}
```

### Create custom validation rules

Implement your own validation rules based on business requirements.


```typescript
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');
}
```