Skip to main content

Backend Integration Guide

This guide demonstrates how to process FramePay tokens on your backend and complete payment transactions using the Payments AI API.

Overview​

Your backend is responsible for:

  • 🔑 Securely storing API credentials
  • 👤 Creating customers
  • 💳 Creating payment instruments (card payments)
  • 💰 Processing transactions
  • ✅ Handling payment results
  • 🔔 Processing webhooks

Security Note: Never expose your API secret key to the frontend. All payment processing must happen on your backend.

Prerequisites​

Before you begin, ensure you have:

  • API Secret Key - Get this from your Payments AI dashboard
  • Organization ID - Provided after merchant registration
  • Backend server - Node.js, Python, PHP, Ruby, etc.

Environment Setup​

Configuration​

Store your credentials securely using environment variables:

# Payments AI Configuration
PAYMENTS_AI_API_KEY=sk_live_your_secret_key_here
PAYMENTS_AI_ORG_ID=org_your_organization_id
PAYMENTS_AI_WEBSITE_ID=web_your_website_id

# Environment
NODE_ENV=production

# API URLs
PAYMENTS_AI_API_URL=https://api.payments.ai
# Use staging for development
# PAYMENTS_AI_API_URL=https://staging-api.payments.ai

Node.js/Express Setup​

// 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.NODE_ENV === 'production'
? 'https://api.payments.ai'
: 'https://staging-api.payments.ai'
};

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

module.exports = config;

HTTP Client Setup​

Axios (Node.js)​

// 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 // 30 seconds
});

// Add request interceptor for logging
paymentsAI.interceptors.request.use(
(config) => {
console.log(`API Request: ${config.method.toUpperCase()} ${config.url}`);
return config;
},
(error) => {
console.error('API Request Error:', error);
return Promise.reject(error);
}
);

// Add response interceptor for error handling
paymentsAI.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
console.error('API Error:', {
status: error.response.status,
data: error.response.data
});
} else {
console.error('Network Error:', error.message);
}
return Promise.reject(error);
}
);

module.exports = paymentsAI;

Python (requests)​

# lib/payments_ai_client.py
import os
import requests
from typing import Dict, Any

class PaymentsAIClient:
def __init__(self):
self.api_key = os.getenv('PAYMENTS_AI_API_KEY')
self.organization_id = os.getenv('PAYMENTS_AI_ORG_ID')
self.api_url = os.getenv('PAYMENTS_AI_API_URL', 'https://api.payments.ai')

if not self.api_key or not self.organization_id:
raise ValueError('Missing Payments AI configuration')

self.headers = {
'Authorization': f'ApiKey {self.api_key}',
'Content-Type': 'application/json'
}

def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
url = f'{self.api_url}{endpoint}'
response = requests.post(url, json=data, headers=self.headers, timeout=30)
response.raise_for_status()
return response.json()

def get(self, endpoint: str) -> Dict[str, Any]:
url = f'{self.api_url}{endpoint}'
response = requests.get(url, headers=self.headers, timeout=30)
response.raise_for_status()
return response.json()

# Create singleton instance
payments_ai = PaymentsAIClient()

API Operations​

Create Customer​

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

async function createCustomer(customerData) {
try {
const response = await paymentsAI.post(
`/v1/organizations/${config.organizationId}/customers`,
{
email: customerData.email,
firstName: customerData.firstName,
lastName: customerData.lastName,
phoneNumber: customerData.phoneNumber,
primaryAddress: {
firstName: customerData.firstName,
lastName: customerData.lastName,
address: customerData.address.address,
city: customerData.address.city,
region: customerData.address.region,
country: customerData.address.country,
zip: customerData.address.postalCode
}
}
);

console.log('Customer created:', response.data.id);
return response.data;

} catch (error) {
console.error('Failed to create customer:', error);
throw new Error('Customer creation failed');
}
}

module.exports = { createCustomer };

Create Payment Instrument​

Used for card payments only:

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

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

console.log('Payment instrument created:', response.data.id);
return response.data;

} catch (error) {
console.error('Failed to create payment instrument:', error);
throw new Error('Payment instrument creation failed');
}
}

module.exports = { createPaymentInstrument };

Create Transaction​

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

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

console.log('Transaction created:', response.data.id);
return response.data;

} catch (error) {
console.error('Failed to create transaction:', error);

if (error.response?.status === 400) {
throw new Error(error.response.data.message || 'Transaction declined');
}

throw new Error('Transaction failed');
}
}

module.exports = { createTransaction };

Payment Processing Endpoints​

Card Payment Endpoint​

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

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

// Validate request
if (!tokenId || !amount || !currency || !customer) {
return res.status(400).json({
success: false,
message: 'Missing required fields'
});
}

try {
// Step 1: Create customer
const customerRecord = await createCustomer({
email: customer.email,
firstName: customer.firstName,
lastName: customer.lastName,
phoneNumber: customer.phoneNumber,
address: customer.address
});

// Step 2: Create transaction with the token directly.
// The customer is present, so this is a CIT and must use a fresh token.
const transaction = await createTransaction({
amount,
currency,
customerId: customerRecord.id,
paymentInstruction: {
token: tokenId // Use the token directly, NOT paymentInstrumentId
},
isMerchantInitiated: false,
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'
}
});

// Check transaction result
if (transaction.result === 'approved') {
res.json({
success: true,
transactionId: transaction.id,
customerId: customerRecord.id,
message: 'Payment successful'
});
} else {
res.status(400).json({
success: false,
message: `Payment ${transaction.result}`,
reason: transaction.declineReason,
transactionId: transaction.id
});
}

} catch (error) {
console.error('Card payment error:', error);
res.status(500).json({
success: false,
message: error.message || 'Payment processing failed'
});
}
});

module.exports = router;

Wallet Payment Endpoint​

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

// Validate request
if (!tokenId || !amount || !currency || !customer) {
return res.status(400).json({
success: false,
message: 'Missing required fields'
});
}

try {
// Step 1: Create customer (with fallback values for incomplete wallet data)
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 || ''
}
});

// Step 2: Create transaction with token directly (skip payment instrument)
const transaction = await createTransaction({
amount,
currency,
customerId: customerRecord.id,
paymentInstruction: {
token: tokenId // Use token directly for wallet payments
},
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 (PayPal)
if (transaction.approvalUrl) {
return res.json({
success: true,
approvalUrl: transaction.approvalUrl,
transactionId: transaction.id,
message: 'Approval required'
});
}

// Check transaction result
if (transaction.result === 'approved') {
res.json({
success: true,
transactionId: transaction.id,
customerId: customerRecord.id,
message: 'Payment successful'
});
} else {
res.status(400).json({
success: false,
message: `Payment ${transaction.result}`,
reason: transaction.declineReason,
transactionId: transaction.id
});
}

} catch (error) {
console.error('Wallet payment error:', error);
res.status(500).json({
success: false,
message: error.message || 'Payment processing failed'
});
}
});

Error Handling​

Comprehensive Error Handler​

// middleware/error-handler.js
function errorHandler(err, req, res, next) {
console.error('Error:', err);

// Payments AI API errors
if (err.response) {
const status = err.response.status;
const data = err.response.data;

switch (status) {
case 400:
return res.status(400).json({
success: false,
message: data.message || 'Bad request',
details: data.details
});

case 401:
return res.status(500).json({
success: false,
message: 'Authentication failed'
});

case 404:
return res.status(404).json({
success: false,
message: 'Resource not found'
});

case 422:
return res.status(422).json({
success: false,
message: 'Validation error',
details: data.details
});

case 429:
return res.status(429).json({
success: false,
message: 'Too many requests. Please try again later.'
});

default:
return res.status(500).json({
success: false,
message: 'Payment processing failed'
});
}
}

// Generic error
res.status(500).json({
success: false,
message: err.message || 'Internal server error'
});
}

module.exports = errorHandler;

Security Best Practices​

1. Validate Requests​

// middleware/validate-payment.js
function validatePaymentRequest(req, res, next) {
const { tokenId, amount, currency, customer } = req.body;

// Validate token
if (!tokenId || typeof tokenId !== 'string') {
return res.status(400).json({
success: false,
message: 'Invalid token'
});
}

// Validate amount
if (!amount || typeof amount !== 'number' || amount <= 0) {
return res.status(400).json({
success: false,
message: 'Invalid amount'
});
}

// Validate currency
const validCurrencies = ['USD', 'EUR', 'GBP', 'CAD', 'AUD'];
if (!currency || !validCurrencies.includes(currency)) {
return res.status(400).json({
success: false,
message: 'Invalid currency'
});
}

// Validate customer data
if (!customer || !customer.email || !customer.firstName || !customer.lastName) {
return res.status(400).json({
success: false,
message: 'Invalid customer data'
});
}

next();
}

module.exports = validatePaymentRequest;

2. Rate Limiting​

// middleware/rate-limit.js
const rateLimit = require('express-rate-limit');

const paymentLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 requests per window
message: {
success: false,
message: 'Too many payment attempts. Please try again later.'
},
standardHeaders: true,
legacyHeaders: false
});

module.exports = paymentLimiter;

3. CORS Configuration​

// middleware/cors.js
const cors = require('cors');

const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? ['https://yourdomain.com']
: ['http://localhost:3000', 'http://localhost:5173'],
methods: ['POST'],
credentials: true,
optionsSuccessStatus: 200
};

module.exports = cors(corsOptions);

Complete Server Setup​

Express Server​

// server.js
require('dotenv').config();
const express = require('express');
const cors = require('./middleware/cors');
const errorHandler = require('./middleware/error-handler');
const paymentRoutes = require('./routes/payments');

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

// Middleware
app.use(cors);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});

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

// Error handler (must be last)
app.use(errorHandler);

// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});

Webhook Integration​

Webhook Endpoint​

// routes/webhooks.js
const express = require('express');
const router = express.Router();
const crypto = require('crypto');

router.post('/payments-ai', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const webhookSecret = process.env.PAYMENTS_AI_WEBHOOK_SECRET;

// Verify webhook signature
const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = hmac.update(req.body).digest('hex');

if (digest !== signature) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(req.body);
console.log('Webhook received:', event.type);

// Handle webhook events
switch (event.type) {
case 'transaction.approved':
handleTransactionApproved(event.data);
break;

case 'transaction.declined':
handleTransactionDeclined(event.data);
break;

case 'transaction.refunded':
handleTransactionRefunded(event.data);
break;

default:
console.log('Unhandled event type:', event.type);
}

res.sendStatus(200);
});

async function handleTransactionApproved(transaction) {
console.log('Transaction approved:', transaction.id);
// Update order status, send confirmation email, etc.
}

async function handleTransactionDeclined(transaction) {
console.log('Transaction declined:', transaction.id);
// Notify customer, update order status, etc.
}

async function handleTransactionRefunded(transaction) {
console.log('Transaction refunded:', transaction.id);
// Update order status, process refund, etc.
}

module.exports = router;

For more information, see Webhook Setup Guide.

Testing​

Test Mode​

Use sandbox credentials for testing:

PAYMENTS_AI_API_KEY=sk_sandbox_test_key
PAYMENTS_AI_API_URL=https://staging-api.payments.ai

Test Request​

curl -X POST http://localhost:3000/api/payments/card \
-H "Content-Type: application/json" \
-d '{
"tokenId": "tok_test_123456",
"amount": 99.99,
"currency": "USD",
"customer": {
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"address": {
"address": "123 Test St",
"city": "Test City",
"region": "TC",
"country": "US",
"postalCode": "12345"
}
}
}'

Logging and Monitoring​

Structured Logging​

// lib/logger.js
const winston = require('winston');

const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});

if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}

module.exports = logger;

Usage​

const logger = require('./lib/logger');

logger.info('Payment initiated', {
tokenId: 'tok_xxx',
amount: 99.99,
currency: 'USD'
});

logger.error('Payment failed', {
error: error.message,
tokenId: 'tok_xxx'
});

Best Practices​

✅ Do​

  • Store API keys in environment variables
  • Validate all incoming requests
  • Implement rate limiting
  • Log payment attempts and errors
  • Use HTTPS in production
  • Verify webhook signatures
  • Handle errors gracefully
  • Test with sandbox credentials first

❌ Don't​

  • Expose API keys in code or frontend
  • Skip input validation
  • Log sensitive data (full card numbers, etc.)
  • Process payments without proper error handling
  • Skip webhook verification
  • Use production keys in development
  • Ignore API errors

Next Steps​

Support​

Need help? Check our: