Skip to main content

Payment Processing Flows

This guide explains the different payment flows used for card and digital wallet payments with Payments AI.

Overview

Payments AI supports two distinct payment flows:

  1. Card Payment Flow - Traditional credit/debit card payments
  2. Wallet Payment Flow - Express checkout with Apple Pay, Google Pay, and PayPal

Understanding these flows is crucial for proper implementation, as they have different processing requirements.

Card Payment Flow

The card payment flow involves creating a payment instrument before processing the transaction.

Flow Diagram

Steps

  1. User fills out form with card details and billing information
  2. Frontend creates token using FramePay
  3. Backend creates customer in Payments AI
  4. Backend creates payment instrument from the token
  5. Backend creates transaction using the payment instrument ID
  6. User sees result (success or failure)

Implementation

Frontend: Create Token

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

const formData = new FormData(event.target);

try {
// Step 1: 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'
});

// Step 2: Send to backend
await processCardPayment(token, formData);

} catch (error) {
console.error('Card payment failed:', error);
showError(error.message);
}
}

async function processCardPayment(token, formData) {
const response = await fetch('/api/payments/card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
tokenId: token.id,
amount: 99.99,
currency: 'USD',
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 = '/payment-success';
} else {
throw new Error(result.message);
}
}

Backend: Process Card Payment

// POST /api/payments/card
async function processCardPayment(req, res) {
const { tokenId, amount, currency, customer } = req.body;

try {
// Step 1: Create customer
const customerResponse = await paymentsAI.post(
`/v1/organizations/${organizationId}/customers`,
{
email: customer.email,
firstName: customer.firstName,
lastName: customer.lastName,
phoneNumber: customer.phoneNumber,
primaryAddress: {
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
}
}
);

const customerId = customerResponse.data.id;

// Step 2: Create payment instrument from token
const instrumentResponse = await paymentsAI.post(
`/v1/organizations/${organizationId}/customers/${customerId}/payment-instruments`,
{
token: tokenId
}
);

const paymentInstrumentId = instrumentResponse.data.id;

// Step 3: Create transaction with payment instrument
const transactionResponse = await paymentsAI.post(
`/v1/organizations/${organizationId}/transactions`,
{
amount,
currency,
customerId,
type: 'sale',
paymentInstruction: {
paymentInstrumentId // Use payment instrument 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
}
}
);

const transaction = transactionResponse.data;

if (transaction.result === 'approved') {
res.json({
success: true,
transactionId: transaction.id,
orderId: transaction.metadata?.orderId
});
} 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'
});
}
}

Key Points

  • ✅ Token → Customer → Payment Instrument → Transaction
  • ✅ Uses paymentInstrumentId in transaction
  • ✅ Payment instrument can be saved for future use
  • ✅ Full billing address required

Wallet Payment Flow

The wallet payment flow skips payment instrument creation and uses the token directly.

Flow Diagram

Steps

  1. User clicks wallet button (Apple Pay, Google Pay, or PayPal)
  2. Wallet authenticates user (Touch ID, Face ID, password)
  3. FramePay emits token automatically via 'token-ready' event
  4. Backend creates customer in Payments AI
  5. Backend creates transaction using the token directly (skip payment instrument)
  6. User sees result or is redirected for approval (PayPal)

Implementation

Frontend: Handle Wallet Token

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

// Check if it's a wallet payment
if (token.method === 'digital-wallet') {
try {
await processWalletPayment(token);
} catch (error) {
console.error('Wallet payment failed:', error);
showError(error.message);
}
}
});

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

const response = await fetch('/api/payments/wallet', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
tokenId: token.id,
paymentMethod: token.paymentInstrument?.type, // 'Apple Pay', 'Google Pay', 'PayPal'
amount: 99.99,
currency: 'USD',
customer: {
// Wallet may provide incomplete data
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();

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

if (result.success) {
window.location.href = '/payment-success';
} else {
throw new Error(result.message);
}
}

Backend: Process Wallet Payment

// POST /api/payments/wallet
async function processWalletPayment(req, res) {
const { tokenId, paymentMethod, amount, currency, customer } = req.body;

try {
// Step 1: Create customer (with fallback values)
const customerResponse = await paymentsAI.post(
`/v1/organizations/${organizationId}/customers`,
{
email: customer.email,
firstName: customer.firstName || 'Customer',
lastName: customer.lastName || 'Name',
phoneNumber: customer.phoneNumber || '',
primaryAddress: {
firstName: customer.firstName || 'Customer',
lastName: customer.lastName || 'Name',
address: customer.address.address || '',
city: customer.address.city || '',
region: customer.address.region || '',
country: customer.address.country || 'US',
zip: customer.address.postalCode || ''
}
}
);

const customerId = customerResponse.data.id;

// Step 2: Create transaction with token directly (skip payment instrument)
const transactionResponse = await paymentsAI.post(
`/v1/organizations/${organizationId}/transactions`,
{
amount,
currency,
customerId,
type: 'sale',
paymentInstruction: {
token: tokenId // Use token directly, NOT paymentInstrumentId
},
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
}
}
);

const transaction = transactionResponse.data;

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

if (transaction.result === 'approved') {
res.json({
success: true,
transactionId: transaction.id,
orderId: transaction.metadata?.orderId
});
} 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'
});
}
}

Key Points

  • ✅ Token → Customer → Transaction (skip payment instrument)
  • ✅ Uses token in transaction, not paymentInstrumentId
  • ✅ Billing address may be incomplete
  • ✅ Some wallets (PayPal) may require approval redirect
  • ✅ Faster checkout experience

Flow Comparison

AspectCard PaymentWallet Payment
Token CreationManual (createToken())Automatic ('token-ready' event)
Payment Instrument✅ Created before transaction❌ Skipped entirely
Transaction Payload{paymentInstrumentId: "inst_xxx"}{token: "tok_xxx"}
Steps4 steps3 steps
Billing AddressComplete (required)May be incomplete
User ExperienceFill form manuallyOne-click checkout
Approval RedirectRare (3DS only)Common (PayPal)
Saved for FutureYes (payment instrument)No (one-time token)

Why Two Flows?

The Problem

Using payment instrument creation for wallet payments consumes the token before the transaction, preventing Payments AI from generating required authentication data (mpiData) for customer-initiated wallet transactions.

The Solution

Wallet payments bypass payment instrument creation and use the token directly in the transaction. This allows Payments AI to:

  • Generate proper mpiData for Apple Pay and PayPal
  • Handle wallet-specific authentication requirements
  • Process the payment correctly through wallet providers

Trade-offs

Advantages:

  • ✅ Wallet payments work correctly
  • ✅ Simpler flow (fewer API calls)
  • ✅ Better UX for express checkout

Disadvantages:

  • ❌ Two code paths to maintain
  • ❌ Wallet payments can't be saved for future use
  • ❌ Requires payment method detection logic

Detecting Payment Method

Use this helper to determine which flow to use:

function getPaymentFlow(token) {
if (token.method === 'digital-wallet' && token.paymentInstrument?.type) {
const walletType = token.paymentInstrument.type;

if (walletType === 'Apple Pay') return 'WALLET';
if (walletType === 'Google Pay') return 'WALLET';
if (walletType === 'PayPal') return 'WALLET';
}

// Default to card flow
return 'CARD';
}

// Usage
const flow = getPaymentFlow(token);

if (flow === 'CARD') {
await processCardPayment(token, formData);
} else {
await processWalletPayment(token);
}

Handling Incomplete Data

Wallet payments may have missing billing address fields:

function normalizeBillingAddress(walletAddress) {
return {
firstName: walletAddress.firstName || 'Customer',
lastName: walletAddress.lastName || 'Name',
address: walletAddress.address || '',
city: walletAddress.city || '',
region: walletAddress.region || '',
country: walletAddress.country || 'US',
postalCode: walletAddress.postalCode || '',
email: walletAddress.emails?.[0]?.value || '[email protected]',
phoneNumber: walletAddress.phoneNumbers?.[0]?.value || ''
};
}

// Usage
const normalizedAddress = normalizeBillingAddress(token.billingAddress);

Note: Always provide fallback values for required fields when processing wallet payments.

Approval Redirects

Some payment methods (especially PayPal) may require user approval:

const transaction = await paymentsAI.createTransaction({...});

// Check for approval URL
if (transaction.approvalUrl) {
// Redirect user to approve payment
window.location.href = transaction.approvalUrl;

// User will be redirected back to your returnUrl
// Handle the return in your callback endpoint
} else if (transaction.result === 'approved') {
// Payment completed immediately
showSuccess();
}

Handling Return from Approval

// GET /payment-callback?transactionId=xxx&status=approved
async function handlePaymentReturn(req, res) {
const { transactionId, status } = req.query;

if (status === 'approved') {
// Fetch transaction details to confirm
const transaction = await paymentsAI.get(
`/v1/organizations/${organizationId}/transactions/${transactionId}`
);

if (transaction.data.result === 'approved') {
res.redirect('/payment-success');
} else {
res.redirect('/payment-failed');
}
} else {
res.redirect('/payment-cancelled');
}
}

Error Handling

Card Payment Errors

try {
await processCardPayment(token, formData);
} catch (error) {
if (error.response?.status === 422) {
// Validation error
showError('Invalid payment information. Please check your details.');
} else if (error.response?.status === 400) {
// Payment declined
showError(`Payment declined: ${error.response.data.declineReason}`);
} else {
// Network or server error
showError('Payment failed. Please try again.');
}
}

Wallet Payment Errors

try {
await processWalletPayment(token);
} catch (error) {
// Wallet errors are usually shown by the wallet provider
// Handle backend errors here
showError('Payment processing failed. Please try again or use a different payment method.');
}

Best Practices

✅ Do

  • Detect payment method from token
  • Use correct flow for each payment type
  • Provide fallback values for wallet payments
  • Handle approval redirects properly
  • Test both flows thoroughly
  • Log payment flow used for debugging

❌ Don't

  • Use payment instruments for wallet payments
  • Assume wallet data is complete
  • Skip approval URL handling
  • Mix flows based on assumption
  • Forget to normalize address data
  • Store wallet tokens for future use

Testing

Test Card Flows

// Simulate card payment
const cardToken = {
id: 'tok_card_123',
method: 'payment-card',
billingAddress: { /* complete data */ },
paymentInstrument: {
brand: 'Visa',
last4: '4242'
}
};

// Should use card flow
const flow = getPaymentFlow(cardToken);
console.assert(flow === 'CARD');

Test Wallet Flows

// Simulate Apple Pay
const applePayToken = {
id: 'tok_applepay_456',
method: 'digital-wallet',
billingAddress: { /* may be incomplete */ },
paymentInstrument: {
type: 'Apple Pay',
brand: 'Visa',
last4: '4242'
}
};

// Should use wallet flow
const flow = getPaymentFlow(applePayToken);
console.assert(flow === 'WALLET');

Next Steps

Resources