Payment Integration Best Practices
Comprehensive guide to implementing secure and reliable payment processing with OpenDev.
Overview
This guide covers best practices for integrating payment providers including Stripe, Google Play Billing, Apple In-App Purchase, and WeChat Pay.
Architecture Patterns
Payment Flow Architecture
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Server │────▶│ OpenDev │────▶│ Provider │
│ App │ │ API │ │ Payment │ │ (Stripe) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
│ 1. Initiate │ │ │
│───────────────▶│ 2. Create │ │
│ │───────────────▶│ 3. Process │
│ │ │───────────────▶│
│ │ │◀───────────────│
│ │◀───────────────│ 4. Confirm │
│◀───────────────│ │ │
│ 5. Complete │ │ │
Webhook Architecture
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Provider │───────────────────▶│ OpenDev │────▶│ Your │
│ Webhook │ 1. Event sent │ Webhook │ │ Handler │
└──────────┘ └──────────┘ └──────────┘
│ │
│ 2. Verify │
│ 3. Forward │
│───────────────▶
│◀──────────────│
│ 4. Process │
Stripe Integration
Server-Side Implementation
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Create payment intent
async function createPayment(amount, currency, customerId) {
const paymentIntent = await stripe.paymentIntents.create({
amount: amount, // In cents
currency: currency,
customer: customerId,
metadata: {
orderId: generateOrderId(),
productId: 'product_123'
}
});
return {
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id
};
}
Webhook Handling
// Webhook endpoint
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
logger.error('Webhook signature verification failed', err);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(event.data.object);
break;
case 'charge.refunded':
await handleRefund(event.data.object);
break;
}
res.json({ received: true });
});
Idempotency
// Use idempotency keys for safe retries
async function createPaymentSafely(amount, currency, orderId) {
const idempotencyKey = `payment_${orderId}`;
return stripe.paymentIntents.create(
{
amount,
currency,
metadata: { orderId }
},
{
idempotencyKey
}
);
}
Google Play Billing
Server-Side Verification
const { google } = require('googleapis');
async function verifyGooglePurchase(packageName, productId, purchaseToken) {
const auth = new google.auth.GoogleAuth({
keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY,
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const androidPublisher = google.androidpublisher({ version: 'v3', auth });
try {
const response = await androidPublisher.purchases.products.get({
packageName,
productId,
token: purchaseToken
});
const purchase = response.data;
// Verify purchase state
if (purchase.purchaseState !== 0) {
throw new Error('Purchase not completed');
}
// Check if already consumed
if (purchase.consumptionState === 1) {
throw new Error('Purchase already consumed');
}
return {
valid: true,
orderId: purchase.orderId,
purchaseTime: purchase.purchaseTimeMillis
};
} catch (error) {
logger.error('Google purchase verification failed', error);
return { valid: false, error: error.message };
}
}
Subscription Handling
async function verifyGoogleSubscription(packageName, subscriptionId, purchaseToken) {
const androidPublisher = google.androidpublisher({ version: 'v3', auth });
const response = await androidPublisher.purchases.subscriptions.get({
packageName,
subscriptionId,
token: purchaseToken
});
const subscription = response.data;
return {
valid: true,
expiryTime: new Date(parseInt(subscription.expiryTimeMillis)),
autoRenewing: subscription.autoRenewing,
cancelReason: subscription.cancelReason
};
}
Apple In-App Purchase
Receipt Verification
const axios = require('axios');
async function verifyAppleReceipt(receiptData, isSandbox = false) {
const url = isSandbox
? 'https://sandbox.itunes.apple.com/verifyReceipt'
: 'https://buy.itunes.apple.com/verifyReceipt';
try {
const response = await axios.post(url, {
'receipt-data': receiptData,
'password': process.env.APPLE_SHARED_SECRET,
'exclude-old-transactions': true
});
const result = response.data;
// Status 21007 means sandbox receipt sent to production
if (result.status === 21007) {
return verifyAppleReceipt(receiptData, true);
}
if (result.status !== 0) {
throw new Error(`Apple verification failed: ${result.status}`);
}
return {
valid: true,
receipt: result.receipt,
latestReceiptInfo: result.latest_receipt_info
};
} catch (error) {
logger.error('Apple receipt verification failed', error);
return { valid: false, error: error.message };
}
}
App Store Server Notifications
// Handle App Store Server Notifications (v2)
app.post('/webhooks/apple', async (req, res) => {
const signedPayload = req.body.signedPayload;
try {
// Verify and decode the JWS
const payload = await verifyAppleJWS(signedPayload);
const notification = JSON.parse(payload);
switch (notification.notificationType) {
case 'DID_RENEW':
await handleSubscriptionRenewal(notification);
break;
case 'DID_FAIL_TO_RENEW':
await handleRenewalFailure(notification);
break;
case 'REFUND':
await handleAppleRefund(notification);
break;
case 'EXPIRED':
await handleSubscriptionExpired(notification);
break;
}
res.status(200).send();
} catch (error) {
logger.error('Apple webhook processing failed', error);
res.status(500).send();
}
});
WeChat Pay
Payment Request
const crypto = require('crypto');
async function createWeChatPayment(orderId, amount, description, openId) {
const params = {
appid: process.env.WECHAT_APP_ID,
mchid: process.env.WECHAT_MCH_ID,
description,
out_trade_no: orderId,
notify_url: process.env.WECHAT_NOTIFY_URL,
amount: {
total: amount, // In cents (分)
currency: 'CNY'
},
payer: {
openid: openId
}
};
const signature = generateWeChatSignature(params);
const response = await axios.post(
'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi',
params,
{
headers: {
'Authorization': `WECHATPAY2-SHA256-RSA2048 ${signature}`,
'Content-Type': 'application/json'
}
}
);
return response.data;
}
Signature Verification
function verifyWeChatSignature(timestamp, nonce, body, signature) {
const message = `${timestamp}\n${nonce}\n${body}\n`;
const verify = crypto.createVerify('RSA-SHA256');
verify.update(message);
return verify.verify(
process.env.WECHAT_PLATFORM_PUBLIC_KEY,
signature,
'base64'
);
}
app.post('/webhooks/wechat', async (req, res) => {
const timestamp = req.headers['wechatpay-timestamp'];
const nonce = req.headers['wechatpay-nonce'];
const signature = req.headers['wechatpay-signature'];
if (!verifyWeChatSignature(timestamp, nonce, JSON.stringify(req.body), signature)) {
return res.status(401).send('Invalid signature');
}
// Process notification
const notification = req.body;
if (notification.event_type === 'TRANSACTION.SUCCESS') {
await handleWeChatPaymentSuccess(notification.resource);
}
res.status(200).send();
});
Security Best Practices
Never Trust Client Data
// WRONG: Using client-provided amount
app.post('/api/payment', async (req, res) => {
const { amount, productId } = req.body;
// amount can be manipulated!
await createPayment(amount, 'usd');
});
// CORRECT: Lookup price from server
app.post('/api/payment', async (req, res) => {
const { productId } = req.body;
const product = await getProduct(productId);
await createPayment(product.price, product.currency);
});
Validate All Purchases
async function fulfillPurchase(purchase) {
// 1. Verify with provider
const verification = await verifyPurchase(purchase);
if (!verification.valid) {
throw new Error('Invalid purchase');
}
// 2. Check for duplicate fulfillment
const existing = await db.purchases.findOne({
where: { transactionId: verification.transactionId }
});
if (existing) {
logger.warn('Duplicate fulfillment attempt', { transactionId });
return existing;
}
// 3. Record and fulfill
const record = await db.purchases.create({
transactionId: verification.transactionId,
userId: purchase.userId,
productId: purchase.productId,
amount: verification.amount,
fulfilledAt: new Date()
});
// 4. Grant entitlement
await grantEntitlement(purchase.userId, purchase.productId);
return record;
}
Secure Webhook Endpoints
// Webhook security middleware
function webhookSecurity(provider) {
return async (req, res, next) => {
try {
// 1. Verify signature
if (!verifySignature(req, provider)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Check timestamp (prevent replay attacks)
const timestamp = getTimestamp(req, provider);
if (Math.abs(Date.now() - timestamp) > 300000) { // 5 minutes
return res.status(401).json({ error: 'Request too old' });
}
// 3. Rate limiting
const ip = req.ip;
if (await isRateLimited(ip, 'webhook')) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
} catch (error) {
logger.error('Webhook security check failed', error);
res.status(500).json({ error: 'Internal error' });
}
};
}
Error Handling
Graceful Degradation
async function processPayment(paymentData) {
const startTime = Date.now();
try {
const result = await paymentProvider.charge(paymentData);
metrics.paymentDuration.observe(Date.now() - startTime);
metrics.paymentSuccess.inc();
return result;
} catch (error) {
metrics.paymentFailure.inc({ reason: error.code });
// Categorize error
if (error.code === 'card_declined') {
throw new PaymentError('CARD_DECLINED', 'Your card was declined');
} else if (error.code === 'expired_card') {
throw new PaymentError('EXPIRED_CARD', 'Your card has expired');
} else if (error.type === 'StripeConnectionError') {
// Retry with exponential backoff
return await retryPayment(paymentData);
}
throw new PaymentError('UNKNOWN', 'Payment failed. Please try again.');
}
}
Refund Handling
async function processRefund(transactionId, reason) {
const transaction = await db.transactions.findByPk(transactionId);
if (!transaction) {
throw new Error('Transaction not found');
}
if (transaction.refundedAt) {
throw new Error('Already refunded');
}
// Process refund with provider
const refund = await paymentProvider.refund(transaction.providerTransactionId);
// Update local records
await db.transactions.update(
{
refundedAt: new Date(),
refundReason: reason,
refundId: refund.id
},
{ where: { id: transactionId } }
);
// Revoke entitlements
await revokeEntitlement(transaction.userId, transaction.productId);
// Notify user
await notifyUser(transaction.userId, 'refund_processed', {
amount: transaction.amount,
currency: transaction.currency
});
return refund;
}
Monitoring & Analytics
Key Metrics
const metrics = {
// Payment metrics
paymentAttempts: new Counter('payment_attempts_total'),
paymentSuccess: new Counter('payment_success_total'),
paymentFailure: new Counter('payment_failure_total'),
paymentAmount: new Histogram('payment_amount_dollars'),
paymentDuration: new Histogram('payment_duration_ms'),
// Subscription metrics
activeSubscriptions: new Gauge('active_subscriptions'),
subscriptionChurn: new Counter('subscription_churn_total'),
// Webhook metrics
webhookReceived: new Counter('webhook_received_total'),
webhookProcessed: new Counter('webhook_processed_total'),
webhookFailed: new Counter('webhook_failed_total')
};
Alerting
// Alert on payment failure spike
if (failureRate > 0.1) { // 10% failure rate
alert.send({
severity: 'critical',
title: 'High Payment Failure Rate',
message: `Payment failure rate is ${(failureRate * 100).toFixed(1)}%`
});
}
// Alert on webhook processing delay
if (webhookProcessingTime > 5000) { // 5 seconds
alert.send({
severity: 'warning',
title: 'Slow Webhook Processing',
message: `Webhook processing time: ${webhookProcessingTime}ms`
});
}
Last updated: January 2026