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
Option 1: CDN (Recommended for quick start)
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 Code | Description | Solution |
|---|---|---|
invalid-payment-card | Card validation failed | Check card number, expiry, CVV |
network-error | Network connection issue | Check internet connection |
invalid-credentials | Invalid publishable key | Verify your API credentials |
token-already-used | Token was already consumed | Create a new token |
validation-error | Missing required fields | Check 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 Number | Scenario | CVV | Expiry |
|---|---|---|---|
| 4242424242424242 | Successful payment | Any 3 digits | Any future date |
| 4000000000000002 | Card declined | Any 3 digits | Any future date |
| 4000000000009995 | Insufficient funds | Any 3 digits | Any future date |
See the complete list of test cards.
Next Steps
- 📖 Backend Integration Guide - Learn how to process tokens on your server
- 🔐 CSP Configuration - Set up Content Security Policy
- 🔄 Payment Flows - Understand card vs wallet payment flows
- 📚 FramePay API Reference - Complete API documentation
- 🎯 Complete Example - Full working implementation
Support
Having issues? Check our: