Payment processing
Handle recurring billing, payment methods, invoicing, and payment failures using the API
This guide covers payment processing for a telecom service from end to end. It takes you through the first collection, the stored payment methods, the billing, and a failed payment.
Prerequisites
You need all of these before you start:
- Order management: Understanding of order creation and pricing flows
- Customer management: Active customers with subscription services
- Payment gateway integration: Access to payment processors (cards, bank transfers, digital wallets)
- Billing system: Understanding of billing cycles and pricing models
- Compliance: PCI DSS compliance for handling payment data
Overview
Payment processing encompasses:
- Payment session creation for secure payment collection
- Payment profile management for stored payment methods
- Payment processing and transaction handling
- Payment failure management and retry logic
- Billing and invoice management
- Promotional pricing and discount handling
Step-by-step implementation
Step 1: Create payment sessions for orders
Create secure payment sessions to collect payment for orders:
# Create payment session for order
curl -X POST "{BASE_URL}/payment-sessions" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderId": "123e4567-e89b-12d3-a456-426614174000",
"paymentProvider": "STRIPE",
"hosted": true,
"returnUrl": "https://yourstore.com/payment/success",
"cancelUrl": "https://yourstore.com/payment/cancel"
}'
# Check payment session status
curl -X GET "{BASE_URL}/payment-sessions/{paymentSessionId}" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"// Create payment session for order
const createOrderPaymentSession = async (orderId, paymentOptions = {}) => {
const sessionData = {
orderId: orderId,
paymentProvider: paymentOptions.paymentProvider || 'STRIPE',
hosted: true,
returnUrl: paymentOptions.returnUrl || `${window.location.origin}/payment/success`,
cancelUrl: paymentOptions.cancelUrl || `${window.location.origin}/payment/cancel`,
metadata: {
source: 'customer_portal',
timestamp: new Date().toISOString(),
},
};
const response = await fetch('{BASE_URL}/payment-sessions', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify(sessionData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Payment session creation failed: ${error.message}`);
}
return await response.json();
};
// Monitor payment session status
const checkPaymentSessionStatus = async (paymentSessionId) => {
const response = await fetch(`{BASE_URL}/payment-sessions/${paymentSessionId}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
const session = await response.json();
return {
status: session.status,
paymentSessionId: session.paymentSessionId,
orderId: session.orderId,
hostedUrl: session.hostedUrl,
updatedAt: session.updatedAt,
};
};
// Look up the customer attached to a session's order
const getOrderCustomer = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
const order = await response.json();
return order.customer;
};
// Payment session component
const PaymentSessionHandler = ({ orderId, onSuccess, onFailure }) => {
const [paymentSession, setPaymentSession] = useState(null);
const [loading, setLoading] = useState(false);
const initiatePayment = async () => {
setLoading(true);
try {
const session = await createOrderPaymentSession(orderId, {
returnUrl: `${window.location.origin}/orders/${orderId}/success`,
cancelUrl: `${window.location.origin}/orders/${orderId}/cancel`,
});
setPaymentSession(session);
// Redirect to payment URL
window.location.href = session.hostedUrl;
} catch (error) {
console.error('Payment session creation failed:', error);
onFailure(error);
} finally {
setLoading(false);
}
};
return (
<div className="payment-session">
<button onClick={initiatePayment} disabled={loading}>
{loading ? 'Creating Payment Session...' : 'Pay Now'}
</button>
</div>
);
};Step 2: Manage payment profiles
Set up stored payment methods for recurring billing:
# Create payment profile session
curl -X POST "{BASE_URL}/payment-profiles/sessions" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"returnUrl": "https://yourstore.com/billing/payment-methods",
"paymentMethods": ["card", "bank_account"]
}'
# Get payment profiles
curl -X GET "{BASE_URL}/payment-profiles" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"// Create payment profile session for storing payment methods
const createPaymentProfileSession = async (returnUrl, customerContext) => {
const sessionData = {
returnUrl: returnUrl,
paymentMethods: ['card', 'bank_account'],
metadata: {
customerId: customerContext.customerId,
source: 'billing_setup',
},
};
const response = await fetch('{BASE_URL}/payment-profiles/sessions', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify(sessionData),
});
return await response.json();
};
// Get stored payment profiles
const getPaymentProfiles = async () => {
const response = await fetch('{BASE_URL}/payment-profiles', {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
return await response.json();
};
// Get specific payment profile
const getPaymentProfile = async (paymentProfileId) => {
const response = await fetch(`{BASE_URL}/payment-profiles/${paymentProfileId}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
return await response.json();
};Step 3: Process payments
Handle payment processing and transaction management:
# List payments
curl -X GET "{BASE_URL}/payments?customerId={customerId}&limit=50" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"
# Get payment details
curl -X GET "{BASE_URL}/payments/{paymentId}" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json"// List payments with filtering
const getPayments = async (filters = {}) => {
const params = new URLSearchParams();
if (filters.customerId) params.append('customerId', filters.customerId);
if (filters.status) params.append('status', filters.status);
if (filters.dateFrom) params.append('dateFrom', filters.dateFrom);
if (filters.dateTo) params.append('dateTo', filters.dateTo);
if (filters.limit) params.append('limit', filters.limit);
if (filters.offset) params.append('offset', filters.offset);
const response = await fetch(`{BASE_URL}/payments?${params}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
return await response.json();
};
// Get specific payment details
const getPaymentDetails = async (paymentId) => {
const response = await fetch(`{BASE_URL}/payments/${paymentId}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
return await response.json();
};
// Payment management component
const PaymentManager = ({ customerId }) => {
const [payments, setPayments] = useState([]);
const [paymentProfiles, setPaymentProfiles] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([getPayments({ customerId, limit: 50 }), getPaymentProfiles()]).then(
([paymentsData, profilesData]) => {
setPayments(paymentsData.items || []);
setPaymentProfiles(profilesData.items || []);
setLoading(false);
},
);
}, [customerId]);
const handleAddPaymentMethod = async () => {
try {
const session = await createPaymentProfileSession(
`${window.location.origin}/billing/payment-methods`,
{ customerId },
);
window.location.href = session.hostedUrl;
} catch (error) {
console.error('Failed to create payment profile session:', error);
}
};
if (loading) return <div>Loading payment information...</div>;
return (
<div className="payment-manager">
<h3>Payment Methods</h3>
<div className="payment-profiles">
{paymentProfiles.map((profile) => (
<div key={profile.paymentProfileId} className="payment-profile">
<span>
{profile.brand} •••• {profile.lastFour}
</span>
<span>
Expires {profile.expiryMonth}/{profile.expiryYear}
</span>
</div>
))}
<button onClick={handleAddPaymentMethod}>Add Payment Method</button>
</div>
<h3>Payment History</h3>
<div className="payment-history">
{payments.map((payment) => (
<div key={payment.paymentId} className="payment-item">
<span>{payment.description}</span>
<span>
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: payment.currency,
}).format(payment.amountMinor / 100)}
</span>
<span className={`status ${payment.status}`}>{payment.status}</span>
<span>{new Date(payment.createdAt).toLocaleDateString()}</span>
</div>
))}
</div>
</div>
);
};Handling payment failures and retries
Handle a failed payment with retry logic:
// Payment failure handler with exponential backoff
class PaymentRetryHandler {
constructor(maxRetries = 3) {
this.maxRetries = maxRetries;
this.retryDelays = [24, 72, 168]; // Hours: 1 day, 3 days, 1 week
}
async handlePaymentFailure(paymentSessionId, failureReason) {
console.log(`Payment failed for session ${paymentSessionId}: ${failureReason}`);
// Get payment session details
const session = await checkPaymentSessionStatus(paymentSessionId);
const customer = await getOrderCustomer(session.orderId);
// Categorize failure type
const failureCategory = this.categorizeFailure(failureReason);
switch (failureCategory) {
case 'insufficient_funds':
await this.scheduleRetry(paymentSessionId, 24); // Retry in 24 hours
await this.notifyCustomer(customer.customerId, 'insufficient_funds');
break;
case 'expired_card':
await this.requestPaymentMethodUpdate(customer.customerId);
break;
case 'fraud_suspected':
await this.escalateToFraud(session);
break;
case 'technical_error':
await this.scheduleRetry(paymentSessionId, 1); // Retry in 1 hour
break;
default:
await this.escalateToSupport(session);
}
}
categorizeFailure(reason) {
const failureMap = {
insufficient_funds: 'insufficient_funds',
card_declined: 'insufficient_funds',
expired_card: 'expired_card',
invalid_cvc: 'expired_card',
fraud_suspected: 'fraud_suspected',
processing_error: 'technical_error',
network_error: 'technical_error',
};
return failureMap[reason] || 'unknown';
}
async scheduleRetry(paymentSessionId, delayHours) {
// In production, this would schedule a background job
setTimeout(
async () => {
try {
// Create new payment session with same order
const originalSession = await checkPaymentSessionStatus(paymentSessionId);
const customer = await getOrderCustomer(originalSession.orderId);
const newSession = await createOrderPaymentSession(originalSession.orderId);
// Notify customer of retry attempt
await this.notifyCustomerRetry(customer.customerId, newSession.hostedUrl);
} catch (error) {
console.error('Payment retry failed:', error);
}
},
delayHours * 60 * 60 * 1000,
);
}
async notifyCustomer(customerId, failureType) {
// Implement customer notification logic
console.log(`Notifying customer ${customerId} about ${failureType}`);
}
async requestPaymentMethodUpdate(customerId) {
// Create payment profile session for updating payment method
const session = await createPaymentProfileSession(
`${process.env.BASE_URL}/billing/update-payment`,
{ customerId },
);
// Send email with update link
await this.notifyCustomer(customerId, 'payment_method_update_required');
}
}Promotional pricing and discounts
Handle promotional codes and discount applications:
// Get promotion by promo code
const getPromotionByCode = async (promoCode) => {
const response = await fetch(`{BASE_URL}/discounts/promotions/promo-code/${promoCode}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Invalid promo code: ${error.message}`);
}
return await response.json();
};
// Apply promo code to an order and re-price it
const applyPromoCodeToOrder = async (orderId, promoCode) => {
try {
// Validate promo code first
const promotion = await getPromotionByCode(promoCode);
// The promo code lives on the order itself
const updateResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
method: 'PUT',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ promoCode }),
});
if (!updateResponse.ok) {
const error = await updateResponse.json();
throw new Error(`Failed to apply promo code: ${error.message}`);
}
// Read the order back to see the promotion applied
const pricingResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
if (!pricingResponse.ok) {
const error = await pricingResponse.json();
throw new Error(`Reading the order failed: ${error.message}`);
}
const { pricing } = await pricingResponse.json();
const discountsMinor = (pricing.lineItems ?? []).reduce(
(sum, item) => sum + (item.totalDiscountsMinor ?? 0),
0,
);
// Amounts are integers in the minor units of pricing.currency: 2749 is $27.49
return {
subtotalMinor: pricing.subtotalMinor,
discountsMinor,
totalMinor: pricing.totalMinor,
currency: pricing.currency,
promotion,
};
} catch (error) {
console.error('Failed to apply promo code:', error);
throw error;
}
};
// Promo code component
const PromoCodeInput = ({ orderId, onApplied, onError }) => {
const [promoCode, setPromoCode] = useState('');
const [loading, setLoading] = useState(false);
const [applied, setApplied] = useState(null);
const handleApply = async () => {
if (!promoCode.trim()) return;
setLoading(true);
try {
const result = await applyPromoCodeToOrder(orderId, promoCode);
setApplied(result);
onApplied(result);
} catch (error) {
onError(error.message);
} finally {
setLoading(false);
}
};
return (
<div className="promo-code-input">
<input
type="text"
placeholder="Enter promo code"
value={promoCode}
onChange={(e) => setPromoCode(e.target.value.toUpperCase())}
disabled={loading || applied}
/>
<button onClick={handleApply} disabled={loading || applied}>
{loading ? 'Applying...' : 'Apply'}
</button>
{applied && (
<div className="promo-applied">
<p>✓ {applied.promotion.discount.description} applied</p>
<p>
Discount: -
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: applied.currency,
}).format(applied.discountsMinor / 100)}
</p>
</div>
)}
</div>
);
};Next steps
After implementing payment processing:
Order fulfillment
Handle service provisioning after successful payment
Customer self-service
A customer manages their own payment methods
Best practices
Security
- Never store raw payment card data - use tokenized payment profiles
- Implement PCI DSS compliance for card processing
- Use HTTPS for all payment-related communications
- Validate all payment webhooks and callbacks
User experience
- Provide clear payment status updates to customers
- Implement user-friendly error messages for payment failures
- Offer multiple payment methods when possible
- Save successful payment methods for future use
Reliability
- Retry a failed payment on a schedule that you control.
- Handle payment processor downtime gracefully
- Monitor payment success rates and failure patterns
- Set up alerts for payment processing issues
Common questions
Q: How do I handle different currencies? A: The API supports more than one currency. Name the currency in the payment session. Your payment processor must support that currency.
Q: Can I process refunds through the API? A: A refund normally goes through your payment processor’s dashboard or API, then reflected in the API payment records.
Q: How do I implement recurring billing? A: Use stored payment profiles with scheduled payment sessions. The billing system can automatically create payment sessions for recurring charges.