Skip to main content

Client-Side Integration Guide

This guide demonstrates how to integrate FramePay on the client side to securely collect payment information and create payment tokens.

Overview

FramePay provides PCI-compliant payment field hosting via secure iframes, ensuring sensitive card data never touches your servers. This guide covers:

  • 🔐 Secure payment form setup
  • 💳 Card payment collection
  • 📱 Digital wallet integration (Apple Pay, Google Pay, PayPal)
  • ✅ Token creation and validation
  • 🎨 Custom styling

Prerequisites

Before you begin, ensure you have:

  • Publishable Key - Get this from your Payments AI dashboard
  • Organization ID - Provided after merchant registration
  • Website ID - Provided after merchant registration

Note: Use sandbox keys for development and testing. Switch to live keys only in production.

Installation

Add the FramePay CSS and JavaScript files to your HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout</title>

<!-- FramePay CSS -->
<link href="https://framepay.payments.ai/framepay.css" rel="stylesheet" />

<!-- FramePay JavaScript -->
<script src="https://framepay.payments.ai/framepay.js"></script>
</head>
<body>
<!-- Your payment form here -->
</body>
</html>

Option 2: NPM Package (For build tools)

If you're using a build tool like Webpack or Vite:

npm install @rebilly/framepay
import { loadFramepay } from '@rebilly/framepay';

const Framepay = await loadFramepay();

Basic Card Payment Form

Step 1: Create the HTML Structure

<form id="payment-form">
<!-- Customer Information -->
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required />
</div>

<div class="form-row">
<div class="form-group">
<label for="firstName">First Name *</label>
<input type="text" id="firstName" name="firstName" required />
</div>

<div class="form-group">
<label for="lastName">Last Name *</label>
<input type="text" id="lastName" name="lastName" required />
</div>
</div>

<!-- Address Information -->
<div class="form-group">
<label for="address">Address *</label>
<input type="text" id="address" name="address" required />
</div>

<div class="form-row">
<div class="form-group">
<label for="city">City *</label>
<input type="text" id="city" name="city" required />
</div>

<div class="form-group">
<label for="region">State/Region *</label>
<input type="text" id="region" name="region" required />
</div>
</div>

<div class="form-row">
<div class="form-group">
<label for="postalCode">ZIP/Postal Code *</label>
<input type="text" id="postalCode" name="postalCode" required />
</div>

<div class="form-group">
<label for="country">Country *</label>
<select id="country" name="country" required>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<!-- Add more countries -->
</select>
</div>
</div>

<div class="form-group">
<label for="phone">Phone *</label>
<input type="tel" id="phone" name="phone" required />
</div>

<!-- Card Information (FramePay will inject iframes here) -->
<div class="form-group">
<label>Card Number *</label>
<div id="card-number" class="framepay-field"></div>
</div>

<div class="form-row">
<div class="form-group">
<label>Expiry Date *</label>
<div id="card-expiry" class="framepay-field"></div>
</div>

<div class="form-group">
<label>CVV *</label>
<div id="card-cvv" class="framepay-field"></div>
</div>
</div>

<button type="submit" id="pay-button">Pay Now</button>
<div id="error-message" class="error-message"></div>
</form>

Step 2: Initialize FramePay

// Configuration
const CONFIG = {
publishableKey: 'pk_sandbox_YOUR_KEY_HERE',
organizationId: 'org_YOUR_ORG_HERE',
websiteId: 'web_YOUR_WEBSITE_HERE',
amount: 99.99,
currency: 'USD'
};

// Initialize FramePay when the page loads
document.addEventListener('DOMContentLoaded', function() {
// Initialize FramePay
Framepay.initialize({
publishableKey: CONFIG.publishableKey,
organizationId: CONFIG.organizationId,
websiteId: CONFIG.websiteId,
transactionData: {
currency: CONFIG.currency,
amount: CONFIG.amount,
label: 'Purchase'
},
style: {
base: {
color: '#1a1a1a',
fontSize: '16px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
'::placeholder': {
color: '#9ca3af'
}
},
invalid: {
color: '#dc2626',
'::placeholder': {
color: '#fca5a5'
}
}
}
});

// Listen for errors
Framepay.on('error', function(error) {
console.error('FramePay Error:', error);
showError('Failed to initialize payment form. Please refresh the page.');
});

// Mount card fields when ready
Framepay.on('ready', function() {
console.log('FramePay is ready');

// Mount separate card fields
Framepay.card.mount('#card-number', 'cardNumber');
Framepay.card.mount('#card-expiry', 'cardExpiration');
Framepay.card.mount('#card-cvv', 'cardCvv');
});

// Handle form submission
const form = document.getElementById('payment-form');
form.addEventListener('submit', handlePaymentSubmit);
});

Step 3: Handle Form Submission and Create Token

async function handlePaymentSubmit(event) {
event.preventDefault();

const payButton = document.getElementById('pay-button');
const errorDiv = document.getElementById('error-message');

// Disable submit button and show loading state
payButton.disabled = true;
payButton.textContent = 'Processing...';
errorDiv.textContent = '';

try {
// Get form data
const formData = new FormData(event.target);

// Create payment token
const token = await Framepay.createToken(event.target, {
billingAddress: {
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
address: formData.get('address'),
city: formData.get('city'),
region: formData.get('region'),
country: formData.get('country'),
postalCode: formData.get('postalCode'),
emails: [{
label: 'main',
value: formData.get('email')
}],
phoneNumbers: [{
label: 'main',
value: formData.get('phone')
}]
},
method: 'payment-card'
});

console.log('Token created:', token.id);

// Send token to your backend
await processPayment(token, formData);

} catch (error) {
console.error('Payment failed:', error);

// Handle specific error codes
if (error.code === 'invalid-payment-card') {
showError('Invalid card details. Please check your card information and try again.');
} else if (error.code === 'network-error') {
showError('Network error. Please check your connection and try again.');
} else {
showError(error.message || 'Payment failed. Please try again.');
}
} finally {
// Re-enable submit button
payButton.disabled = false;
payButton.textContent = 'Pay Now';
}
}

function showError(message) {
const errorDiv = document.getElementById('error-message');
errorDiv.textContent = message;
}

Step 4: Send Token to Backend

async function processPayment(token, formData) {
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokenId: token.id,
amount: CONFIG.amount,
currency: CONFIG.currency,
customer: {
email: formData.get('email'),
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
phoneNumber: formData.get('phone'),
address: {
address: formData.get('address'),
city: formData.get('city'),
region: formData.get('region'),
country: formData.get('country'),
postalCode: formData.get('postalCode')
}
}
})
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Payment processing failed');
}

const result = await response.json();

if (result.success) {
// Redirect to success page
window.location.href = '/payment-success?orderId=' + result.orderId;
} else {
throw new Error(result.message || 'Payment was declined');
}
}

Digital Wallet Integration

Important: Before implementing digital wallets in your code, you must configure them in your gateway account. See the Express Methods guide for detailed gateway configuration steps for each wallet method.

Adding Express Checkout Buttons

Digital wallets (Apple Pay, Google Pay, PayPal) provide a faster checkout experience by auto-filling customer information.

<!-- Add wallet buttons above the card form -->
<div class="wallet-buttons">
<div id="apple-pay-button" class="wallet-button"></div>
<div id="google-pay-button" class="wallet-button"></div>
<div id="paypal-button" class="wallet-button"></div>
</div>

<div class="divider">
<span>Or pay with card</span>
</div>

<!-- Card form continues here -->

Initialize with Wallet Support

Framepay.initialize({
publishableKey: CONFIG.publishableKey,
organizationId: CONFIG.organizationId,
websiteId: CONFIG.websiteId,
transactionData: {
currency: CONFIG.currency,
amount: CONFIG.amount,
label: 'Purchase from Your Store',
requestShipping: false // Set to true if you need shipping info
},
// Apple Pay configuration
applePay: {
buttonHeight: '48px',
buttonStyle: 'black' // or 'white', 'white-outline'
},
// Google Pay configuration
googlePay: {
buttonHeight: '48px',
buttonColor: 'default' // or 'black', 'white'
},
// PayPal configuration
paypal: {
buttonHeight: 48,
style: {
layout: 'horizontal',
color: 'gold',
shape: 'rect',
label: 'paypal'
}
}
});

Framepay.on('ready', function() {
// Mount wallet buttons
Framepay.applePay.mount('#apple-pay-button');
Framepay.googlePay.mount('#google-pay-button');
Framepay.paypal.mount('#paypal-button');

// Mount card fields
Framepay.card.mount('#card-number', 'cardNumber');
Framepay.card.mount('#card-expiry', 'cardExpiration');
Framepay.card.mount('#card-cvv', 'cardCvv');
});

Handle Wallet Tokens

Wallet payments emit tokens automatically when the user completes the wallet flow:

Framepay.on('token-ready', async function(token, extraData) {
console.log('Wallet token received:', token);
console.log('Extra data:', extraData);

// Detect payment method
const isWallet = token.method === 'digital-wallet';

if (isWallet) {
try {
// For wallet payments, use the token directly
await processWalletPayment(token);
} catch (error) {
console.error('Wallet payment failed:', error);
showError('Payment failed: ' + error.message);
}
}
});

async function processWalletPayment(token) {
const billingAddress = token.billingAddress || {};

const response = await fetch('/api/process-wallet-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokenId: token.id,
amount: CONFIG.amount,
currency: CONFIG.currency,
paymentMethod: token.paymentInstrument?.type, // 'Apple Pay', 'Google Pay', or 'PayPal'
customer: {
email: billingAddress.emails?.[0]?.value || '[email protected]',
firstName: billingAddress.firstName || 'Customer',
lastName: billingAddress.lastName || 'Name',
phoneNumber: billingAddress.phoneNumbers?.[0]?.value || '',
address: {
address: billingAddress.address || '',
city: billingAddress.city || '',
region: billingAddress.region || '',
country: billingAddress.country || 'US',
postalCode: billingAddress.postalCode || ''
}
}
})
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Payment processing failed');
}

const result = await response.json();

// Some wallet payments may require approval (e.g., PayPal)
if (result.approvalUrl) {
window.location.href = result.approvalUrl;
return;
}

if (result.success) {
window.location.href = '/payment-success?orderId=' + result.orderId;
} else {
throw new Error(result.message || 'Payment was declined');
}
}

Updating Transaction Amount Dynamically

If your cart amount changes (e.g., applying a discount), update the transaction data:

function updateCartTotal(newAmount) {
CONFIG.amount = newAmount;

// Update FramePay with new amount
Framepay.update({
transactionData: {
currency: CONFIG.currency,
amount: newAmount,
label: 'Purchase'
}
}).then(function() {
console.log('Transaction amount updated to:', newAmount);

// Update UI
document.getElementById('total-amount').textContent =
`$${newAmount.toFixed(2)}`;
}).catch(function(error) {
console.error('Failed to update amount:', error);
});
}

// Example: Apply discount
document.getElementById('apply-discount').addEventListener('click', function() {
const discountedAmount = CONFIG.amount * 0.9; // 10% off
updateCartTotal(discountedAmount);
});

Custom Styling

Basic Theme Customization

Framepay.initialize({
publishableKey: CONFIG.publishableKey,
// ... other config
style: {
base: {
color: '#1a1a1a',
fontSize: '16px',
fontFamily: 'system-ui, sans-serif',
fontWeight: '400',
lineHeight: '1.5',
padding: '12px 16px',
borderRadius: '8px',
border: '1px solid #d1d5db',
backgroundColor: '#ffffff',
transition: 'border-color 0.2s',
'::placeholder': {
color: '#9ca3af',
fontWeight: '400'
}
},
focus: {
borderColor: '#3b82f6',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)'
},
invalid: {
color: '#dc2626',
borderColor: '#ef4444',
'::placeholder': {
color: '#fca5a5'
}
},
valid: {
borderColor: '#10b981'
}
}
});

Dark Mode Support

// Detect system theme preference
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;

Framepay.initialize({
publishableKey: CONFIG.publishableKey,
// ... other config
style: {
base: {
color: isDarkMode ? '#f3f4f6' : '#1a1a1a',
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
border: `1px solid ${isDarkMode ? '#374151' : '#d1d5db'}`,
fontSize: '16px',
fontFamily: 'system-ui, sans-serif',
'::placeholder': {
color: isDarkMode ? '#6b7280' : '#9ca3af'
}
},
focus: {
borderColor: '#3b82f6'
},
invalid: {
color: '#ef4444',
borderColor: '#ef4444'
}
}
});

Complete CSS Example

/* Form styling */
.form-group {
margin-bottom: 1rem;
}

.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}

label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #374151;
}

input, select {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
transition: border-color 0.2s;
}

input:focus, select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

/* FramePay field containers */
.framepay-field {
min-height: 48px;
border-radius: 0.5rem;
}

/* Submit button */
#pay-button {
width: 100%;
padding: 0.875rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 1.5rem;
}

#pay-button:hover {
background-color: #2563eb;
}

#pay-button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}

/* Error message */
.error-message {
color: #dc2626;
font-size: 0.875rem;
margin-top: 0.5rem;
min-height: 1.25rem;
}

/* Wallet buttons */
.wallet-buttons {
display: grid;
gap: 0.75rem;
margin-bottom: 1.5rem;
}

.wallet-button {
min-height: 48px;
}

.divider {
display: flex;
align-items: center;
text-align: center;
margin: 1.5rem 0;
color: #6b7280;
font-size: 0.875rem;
}

.divider::before,
.divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #e5e7eb;
}

.divider span {
padding: 0 1rem;
}

Token Structure Reference

Card Token

{
"id": "tok_01abc123xyz",
"method": "payment-card",
"isUsed": false,
"createdTime": "2024-01-15T10:30:00Z",
"billingAddress": {
"firstName": "John",
"lastName": "Doe",
"address": "123 Main St",
"city": "New York",
"region": "NY",
"country": "US",
"postalCode": "10001",
"emails": [{"label": "main", "value": "[email protected]"}],
"phoneNumbers": [{"label": "main", "value": "+1234567890"}]
},
"paymentInstrument": {
"brand": "Visa",
"last4": "4242",
"expMonth": 12,
"expYear": 2025,
"bin": "424242"
}
}

Wallet Token

{
"id": "tok_01wallet456",
"method": "digital-wallet",
"isUsed": false,
"paymentInstrument": {
"type": "Apple Pay",
"descriptor": "Apple Pay (Visa ****4242)",
"brand": "Visa",
"last4": "4242",
"expMonth": 12,
"expYear": 2025
},
"billingAddress": {
"firstName": null,
"lastName": null,
"city": "New York",
"region": "NY",
"country": "US",
"postalCode": "10001"
}
}

Note: Wallet tokens may have incomplete billing address data. Always provide fallback values when processing wallet payments.

Error Handling

Common Error Codes

Error CodeDescriptionSolution
invalid-payment-cardCard validation failedCheck card number, expiry, CVV
network-errorNetwork connection issueCheck internet connection
invalid-credentialsInvalid publishable keyVerify your API credentials
token-already-usedToken was already consumedCreate a new token
validation-errorMissing required fieldsCheck billing address fields

Error Handling Example

try {
const token = await Framepay.createToken(form, extraData);
await processPayment(token);
} catch (error) {
console.error('Error:', error);

switch (error.code) {
case 'invalid-payment-card':
showError('Invalid card details. Please check your card information.');
break;
case 'network-error':
showError('Network error. Please check your connection and try again.');
break;
case 'validation-error':
showError('Please fill in all required fields correctly.');
break;
default:
showError(error.message || 'An error occurred. Please try again.');
}
}

Best Practices

Security

  • Always use HTTPS in production
  • Never log or store card details on your server
  • Use sandbox keys for development
  • Implement proper CSP headers (see CSP Configuration Guide)
  • Validate input on both client and server side

UX Considerations

  • Show loading states during token creation
  • Provide clear error messages to users
  • Disable submit button during processing
  • Auto-focus first field on page load
  • Support keyboard navigation
  • Test on mobile devices

Performance

  • Load FramePay script early in the page
  • Initialize on DOMContentLoaded
  • Debounce amount updates if cart changes frequently
  • Minimize rerenders of payment form

Testing

Use these test cards in sandbox mode:

Card NumberScenarioCVVExpiry
4242424242424242Successful paymentAny 3 digitsAny future date
4000000000000002Card declinedAny 3 digitsAny future date
4000000000009995Insufficient fundsAny 3 digitsAny future date

See the complete list of test cards.

Next Steps

Support

Having issues? Check our: