Skip to main content

Complete Working Example

This page provides a full, production-ready implementation of FramePay with Payments AI, including both frontend and backend code.

Overview

This example demonstrates:

  • ✅ Card and wallet payment support
  • ✅ Proper payment flow handling
  • ✅ Error handling and validation
  • ✅ CSP configuration
  • ✅ Production-ready code structure
  • ✅ Modern UI with responsive design

Project Structure

payment-integration/
├── frontend/
│ ├── index.html
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── payment.js
├── backend/
│ ├── server.js
│ ├── config/
│ │ └── payments-ai.js
│ ├── lib/
│ │ └── payments-ai-client.js
│ ├── routes/
│ │ └── payments.js
│ ├── services/
│ │ ├── customer-service.js
│ │ ├── payment-instrument-service.js
│ │ └── transaction-service.js
│ └── middleware/
│ ├── validate-payment.js
│ └── error-handler.js
├── .env
└── package.json

Backend Implementation

1. Environment Configuration

# .env
PAYMENTS_AI_API_KEY=sk_sandbox_your_secret_key
PAYMENTS_AI_ORG_ID=org_your_organization_id
PAYMENTS_AI_WEBSITE_ID=web_your_website_id
PAYMENTS_AI_API_URL=https://staging-api.payments.ai
NODE_ENV=development
PORT=3000

2. Configuration

// backend/config/payments-ai.js
require('dotenv').config();

const config = {
apiKey: process.env.PAYMENTS_AI_API_KEY,
organizationId: process.env.PAYMENTS_AI_ORG_ID,
websiteId: process.env.PAYMENTS_AI_WEBSITE_ID,
apiUrl: process.env.PAYMENTS_AI_API_URL || 'https://api.payments.ai',
};

if (!config.apiKey || !config.organizationId) {
throw new Error('Missing required Payments AI configuration');
}

module.exports = config;

3. API Client

// backend/lib/payments-ai-client.js
const axios = require('axios');
const config = require('../config/payments-ai');

const paymentsAI = axios.create({
baseURL: config.apiUrl,
headers: {
Authorization: `ApiKey ${config.apiKey}`,
'Content-Type': 'application/json',
},
timeout: 30000,
});

paymentsAI.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
console.error('API Error:', {
status: error.response.status,
message: error.response.data,
});
}
return Promise.reject(error);
}
);

module.exports = paymentsAI;

4. Services

// backend/services/customer-service.js
const paymentsAI = require('../lib/payments-ai-client');
const config = require('../config/payments-ai');

async function createCustomer(customerData) {
const customerAddress = {
address: customerData.address.address,
city: customerData.address.city,
region: customerData.address.region,
country: customerData.address.country,
zip: customerData.address.postalCode || customerData.address.zip,
};

// Only include address2 if it's provided
if (customerData.address.address2) {
customerAddress.address2 = customerData.address.address2;
}

const response = await paymentsAI.post(
`/v1/organizations/${config.organizationId}/customers`,
{
email: customerData.email,
firstName: customerData.firstName,
lastName: customerData.lastName,
phoneNumber: customerData.phoneNumber,
primaryAddress: {
customerAddress,
},
isEnhancedDueDiligenceRequired: false,
}
);
return response.data;
}

module.exports = { createCustomer };
// backend/services/payment-instrument-service.js
const paymentsAI = require('../lib/payments-ai-client');
const config = require('../config/payments-ai');

async function createPaymentInstrument(customerId, tokenId) {
const response = await paymentsAI.post(
`/v1/organizations/${config.organizationId}/customers/${customerId}/payment-instruments`,
{ token: tokenId }
);
return response.data;
}

module.exports = { createPaymentInstrument };
// backend/services/transaction-service.js
const paymentsAI = require('../lib/payments-ai-client');
const config = require('../config/payments-ai');

async function createTransaction(transactionData) {
const response = await paymentsAI.post(
`/v1/organizations/${config.organizationId}/transactions`,
{
amount: transactionData.amount,
currency: transactionData.currency,
customerId: transactionData.customerId,
type: 'sale',
paymentInstruction: transactionData.paymentInstruction,
billingAddress: transactionData.billingAddress,
metadata: transactionData.metadata || {},
}
);
return response.data;
}

module.exports = { createTransaction };

5. Payment Routes

// backend/routes/payments.js
const express = require('express');
const router = express.Router();
const { createCustomer } = require('../services/customer-service');
const {
createPaymentInstrument,
} = require('../services/payment-instrument-service');
const { createTransaction } = require('../services/transaction-service');

// Card payment
router.post('/card', async (req, res) => {
const { tokenId, amount, currency, customer } = req.body;

try {
// Create customer
const customerRecord = await createCustomer(customer);

// Create payment instrument
const paymentInstrument = await createPaymentInstrument(
customerRecord.id,
tokenId
);

// Create transaction
const transaction = await createTransaction({
amount,
currency,
customerId: customerRecord.id,
paymentInstruction: {
paymentInstrumentId: paymentInstrument.id,
},
billingAddress: {
firstName: customer.firstName,
lastName: customer.lastName,
address: customer.address.address,
city: customer.address.city,
region: customer.address.region,
country: customer.address.country,
zip: customer.address.postalCode,
email: customer.email,
phoneNumber: customer.phoneNumber,
},
metadata: {
source: 'web-checkout',
paymentMethod: 'card',
},
});

if (transaction.result === 'approved') {
res.json({
success: true,
transactionId: transaction.id,
message: 'Payment successful',
});
} else {
res.status(400).json({
success: false,
message: `Payment ${transaction.result}`,
reason: transaction.declineReason,
});
}
} catch (error) {
console.error('Card payment error:', error);
res.status(500).json({
success: false,
message: error.message || 'Payment processing failed',
});
}
});

// Wallet payment
router.post('/wallet', async (req, res) => {
const { tokenId, paymentMethod, amount, currency, customer } = req.body;

try {
// Create customer with fallback values
const customerRecord = await createCustomer({
email: customer.email || '[email protected]',
firstName: customer.firstName || 'Customer',
lastName: customer.lastName || 'Name',
phoneNumber: customer.phoneNumber || '',
address: {
address: customer.address.address || '',
city: customer.address.city || '',
region: customer.address.region || '',
country: customer.address.country || 'US',
postalCode: customer.address.postalCode || '',
},
});

// Create transaction with token directly
const transaction = await createTransaction({
amount,
currency,
customerId: customerRecord.id,
paymentInstruction: {
token: tokenId, // Use token directly
},
billingAddress: {
firstName: customer.firstName || 'Customer',
lastName: customer.lastName || 'Name',
address: customer.address.address,
city: customer.address.city,
region: customer.address.region,
country: customer.address.country,
zip: customer.address.postalCode,
email: customer.email,
phoneNumber: customer.phoneNumber,
},
metadata: {
source: 'web-checkout',
paymentMethod: paymentMethod || 'digital-wallet',
},
});

// Handle approval redirect
if (transaction.approvalUrl) {
return res.json({
success: true,
approvalUrl: transaction.approvalUrl,
transactionId: transaction.id,
});
}

if (transaction.result === 'approved') {
res.json({
success: true,
transactionId: transaction.id,
message: 'Payment successful',
});
} else {
res.status(400).json({
success: false,
message: `Payment ${transaction.result}`,
reason: transaction.declineReason,
});
}
} catch (error) {
console.error('Wallet payment error:', error);
res.status(500).json({
success: false,
message: error.message || 'Payment processing failed',
});
}
});

module.exports = router;

6. Server

// backend/server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const paymentRoutes = require('./routes/payments');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(
cors({
origin:
process.env.NODE_ENV === 'production'
? ['https://yourdomain.com']
: ['http://localhost:5173', 'http://localhost:3000'],
credentials: true,
})
);
app.use(express.json());
app.use(express.static('frontend'));

// Routes
app.use('/api/payments', paymentRoutes);

// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
success: false,
message: 'Internal server error',
});
});

app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

7. Package.json

{
"name": "framepay-payments-ai-example",
"version": "1.0.0",
"description": "Complete FramePay integration with Payments AI",
"main": "backend/server.js",
"scripts": {
"start": "node backend/server.js",
"dev": "nodemon backend/server.js"
},
"dependencies": {
"axios": "^1.6.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

Frontend Implementation

1. HTML

<!-- frontend/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkout - FramePay Integration</title>

<!-- CSP -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-inline' framepay.payments.ai *.paypal.com *.google.com *.cdn-apple.com;
connect-src 'self' framepay.payments.ai *.payments.ai *.paypal.com *.google.com;
style-src 'self' 'unsafe-inline' framepay.payments.ai fonts.googleapis.com;
frame-src 'self' framepay.payments.ai *.paypal.com *.google.com;
img-src 'self' data: framepay.payments.ai *.paypal.com *.google.com;
font-src 'self' fonts.gstatic.com;
"
/>

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

<!-- Styles -->
<link href="css/style.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<div class="checkout-header">
<h1>Checkout</h1>
<div class="amount">Total: <span id="total-amount">$99.99</span></div>
</div>

<!-- Express Checkout -->
<section class="express-checkout">
<h2>Express Checkout</h2>
<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>
</section>

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

<!-- Card Payment Form -->
<section class="card-payment">
<form id="payment-form">
<!-- Contact Information -->
<fieldset>
<legend>Contact Information</legend>

<div class="form-group">
<label for="email">Email *</label>
<input
type="email"
id="email"
name="email"
required
placeholder="[email protected]"
/>
</div>
</fieldset>

<!-- Billing Information -->
<fieldset>
<legend>Billing Information</legend>

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

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

<div class="form-group">
<label for="address">Address *</label>
<input
type="text"
id="address"
name="address"
required
placeholder="123 Main St"
/>
</div>

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

<div class="form-group">
<label for="region">State/Region *</label>
<input
type="text"
id="region"
name="region"
required
placeholder="NY"
/>
</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
placeholder="10001"
/>
</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>
<option value="AU">Australia</option>
</select>
</div>
</div>

<div class="form-group">
<label for="phone">Phone *</label>
<input
type="tel"
id="phone"
name="phone"
required
placeholder="+1 (555) 123-4567"
/>
</div>
</fieldset>

<!-- Card Information -->
<fieldset>
<legend>Card Information</legend>

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

<div id="error-message" class="error-message"></div>

<button type="submit" id="pay-button" class="pay-button">
Pay $99.99
</button>
</form>
</section>
</div>

<script src="js/payment.js"></script>
</body>
</html>

2. CSS

/* frontend/css/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f9fafb;
color: #1f2937;
line-height: 1.6;
}

.container {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
}

.checkout-header {
background: white;
padding: 2rem;
border-radius: 0.75rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.checkout-header h1 {
font-size: 1.875rem;
margin-bottom: 0.5rem;
}

.amount {
font-size: 1.5rem;
font-weight: 600;
color: #3b82f6;
}

.express-checkout,
.card-payment {
background: white;
padding: 2rem;
border-radius: 0.75rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.express-checkout h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
}

.wallet-buttons {
display: grid;
gap: 0.75rem;
}

.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;
}

fieldset {
border: none;
margin-bottom: 1.5rem;
}

legend {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
}

.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;
font-size: 0.875rem;
}

input,
select {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
transition:
border-color 0.2s,
box-shadow 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 {
min-height: 48px;
border-radius: 0.5rem;
}

.pay-button {
width: 100%;
padding: 1rem;
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 {
color: #dc2626;
font-size: 0.875rem;
margin-top: 1rem;
min-height: 1.25rem;
padding: 0.5rem;
border-radius: 0.375rem;
background-color: #fee2e2;
display: none;
}

.error-message:not(:empty) {
display: block;
}

@media (max-width: 640px) {
.form-row {
grid-template-columns: 1fr;
}
}

3. JavaScript

// frontend/js/payment.js
const CONFIG = {
publishableKey: 'pk_sandbox_YOUR_KEY', // Replace with your key
organizationId: 'org_YOUR_ORG', // Replace with your org ID
websiteId: 'web_YOUR_WEBSITE', // Replace with your website ID
amount: 99.99,
currency: 'USD',
apiUrl: 'http://localhost:3000/api/payments',
};

document.addEventListener('DOMContentLoaded', function () {
initializeFramePay();
setupFormHandling();
});

function initializeFramePay() {
Framepay.initialize({
publishableKey: CONFIG.publishableKey,
organizationId: CONFIG.organizationId,
websiteId: CONFIG.websiteId,
transactionData: {
currency: CONFIG.currency,
amount: CONFIG.amount,
label: 'Purchase',
requestShipping: false,
},
style: {
base: {
color: '#1f2937',
fontSize: '16px',
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
'::placeholder': {
color: '#9ca3af',
},
},
focus: {
borderColor: '#3b82f6',
},
invalid: {
color: '#dc2626',
'::placeholder': {
color: '#fca5a5',
},
},
},
applePay: {
buttonHeight: '48px',
buttonStyle: 'black',
},
googlePay: {
buttonHeight: '48px',
buttonColor: 'default',
},
paypal: {
buttonHeight: 48,
style: {
layout: 'horizontal',
color: 'gold',
shape: 'rect',
label: 'paypal',
},
},
});

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

Framepay.on('ready', function () {
console.log('FramePay is ready');

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

// Mount wallet buttons
Framepay.applePay.mount('#apple-pay-button');
Framepay.googlePay.mount('#google-pay-button');
Framepay.paypal.mount('#paypal-button');
});

// Handle wallet tokens
Framepay.on('token-ready', async function (token, extraData) {
console.log('Wallet token received:', token);

if (token.method === 'digital-wallet') {
await handleWalletPayment(token);
}
});
}

function setupFormHandling() {
const form = document.getElementById('payment-form');
form.addEventListener('submit', handleCardPayment);
}

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

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

payButton.disabled = true;
payButton.textContent = 'Processing...';
errorDiv.textContent = '';

try {
const formData = new FormData(event.target);

// Create 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);

// Process payment
const response = await fetch(`${CONFIG.apiUrl}/card`, {
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'),
},
},
}),
});

const result = await response.json();

if (result.success) {
window.location.href = '/success.html?txn=' + result.transactionId;
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('Payment failed:', error);

if (error.code === 'invalid-payment-card') {
showError('Invalid card details. Please check your card information.');
} else {
showError(error.message || 'Payment failed. Please try again.');
}
} finally {
payButton.disabled = false;
payButton.textContent = 'Pay $' + CONFIG.amount.toFixed(2);
}
}

async function handleWalletPayment(token) {
showError(''); // Clear errors

try {
const billingAddress = token.billingAddress || {};

const response = await fetch(`${CONFIG.apiUrl}/wallet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tokenId: token.id,
paymentMethod: token.paymentInstrument?.type,
amount: CONFIG.amount,
currency: CONFIG.currency,
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 || '',
},
},
}),
});

const result = await response.json();

// Handle approval redirect
if (result.approvalUrl) {
window.location.href = result.approvalUrl;
return;
}

if (result.success) {
window.location.href = '/success.html?txn=' + result.transactionId;
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('Wallet payment failed:', error);
showError(error.message || 'Payment failed. Please try again.');
}
}

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

Running the Example

1. Install Dependencies

npm install

2. Configure Environment

Update .env with your credentials:

PAYMENTS_AI_API_KEY=sk_sandbox_your_key
PAYMENTS_AI_ORG_ID=org_your_org
PAYMENTS_AI_WEBSITE_ID=web_your_website

3. Update Frontend Config

Update frontend/js/payment.js:

const CONFIG = {
publishableKey: 'pk_sandbox_YOUR_KEY', // Your publishable key
organizationId: 'org_YOUR_ORG', // Your organization ID
websiteId: 'web_YOUR_WEBSITE', // Your website ID
// ...
};

4. Start Server

npm start

5. Open in Browser

Navigate to: http://localhost:3000

Testing

Test Cards

Card NumberResult
4242424242424242Success
4000000000000002Declined
4000000000009995Insufficient Funds

Test Wallets

  • Apple Pay: Use Safari on Mac/iOS with test card
  • Google Pay: Use Chrome with test card
  • PayPal: Use PayPal sandbox credentials

Production Checklist

Before deploying to production:

  • Switch to live API keys
  • Update API URL to production
  • Enable HTTPS
  • Configure proper CORS
  • Set up error logging
  • Configure webhooks
  • Test all payment methods
  • Add rate limiting
  • Review CSP settings
  • Set up monitoring

Next Steps

Download

You can download the complete example from our GitHub repository (if available).

Support

Need help?