Best Practices
Production-ready patterns for building secure, performant, and scalable ZendFi integrations.
Security
Never Expose API Keys
Don't do this:
// NEVER expose API keys in client-side code
const zendfi = new ZendFiClient({
apiKey: 'zfi_live_abc123...' // Visible in browser!
});
Do this instead:
- Next.js
- Express
// app/api/payments/route.ts (Server-side)
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient(); // Reads from server env vars
export async function POST(request: Request) {
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD',
});
return Response.json({ payment_url: payment.payment_url });
}
// app/components/CheckoutButton.tsx (Client-side)
'use client';
export function CheckoutButton() {
async function handleCheckout() {
// Call your API route, not ZendFi directly
const response = await fetch('/api/payments', {
method: 'POST',
body: JSON.stringify({ amount: 99 })
});
const { payment_url } = await response.json();
window.location.href = payment_url;
}
return <button onClick={handleCheckout}>Pay Now</button>;
}
// server.ts (Server-side only)
import express from 'express';
import { ZendFiClient } from '@zendfi/sdk';
const app = express();
const zendfi = new ZendFiClient(); // Server-side only
app.post('/api/payments', async (req, res) => {
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD',
});
res.json({ payment_url: payment.payment_url });
});
Use Environment Variables
# .env.local (Never commit this file!)
ZENDFI_API_KEY=zfi_live_abc123...
ZENDFI_WEBHOOK_SECRET=whsec_xyz789...
# .gitignore
.env
.env.local
.env.*.local
Verify Webhook Signatures
Always verify webhook signatures to prevent spoofing:
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient();
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('x-zendfi-signature');
if (!signature) {
return new Response('Missing signature', { status: 401 });
}
try {
// Verify signature
const isValid = zendfi.verifyWebhook({
payload: body,
signature,
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
});
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
// Parse verified webhook
const event = JSON.parse(body);
// Process verified event
await handleEvent(event);
return new Response('OK', { status: 200 });
} catch (err) {
console.error('Webhook verification failed:', err);
return new Response('Invalid signature', { status: 401 });
}
}
Or use the helper function for Next.js:
import { verifyNextWebhook } from '@zendfi/sdk/webhooks';
export async function POST(request: Request) {
const webhook = await verifyNextWebhook(request);
if (!webhook) {
return new Response('Invalid signature', { status: 401 });
}
// Process verified webhook
await handleEvent(webhook);
return new Response('OK', { status: 200 });
}
Sanitize Metadata
Don't store sensitive data in metadata - it may be logged or visible in dashboard:
Don't do this:
const payment = await zendfi.createPayment({
amount: 100,
metadata: {
credit_card: '4242-4242-4242-4242', // Never!
ssn: '123-45-6789', // Never!
password: 'secret123' // Never!
}
});
Do this instead:
const payment = await zendfi.createPayment({
amount: 100,
metadata: {
order_id: 'order_12345',
user_id: 'user_xyz789',
product_sku: 'PROD-001'
}
});
Implement Rate Limiting
Protect your endpoints from abuse:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max 100 requests per window
message: 'Too many requests, please try again later'
});
app.post('/api/payments', limiter, async (req, res) => {
// Your payment logic
});
Validate Input
Always validate user input before creating payments:
import { z } from 'zod';
const PaymentSchema = z.object({
amount: z.number().positive().max(1000000),
currency: z.enum(['USD']),
description: z.string().min(1).max(500),
});
export async function POST(request: Request) {
const body = await request.json();
// Validate input
const validated = PaymentSchema.parse(body);
const payment = await zendfi.createPayment(validated);
return Response.json({ payment_url: payment.payment_url });
}
Error Handling
Use Try-Catch Blocks
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient();
try {
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD'
});
return { success: true, payment_url: payment.payment_url };
} catch (error) {
// Log error for debugging
console.error('Payment creation failed:', error);
// Return user-friendly message
return {
success: false,
error: 'Unable to create payment. Please try again.'
};
}
Handle Specific Error Types
import { ZendFiClient, ZendFiError } from '@zendfi/sdk';
const zendfi = new ZendFiClient();
try {
const payment = await zendfi.createPayment({ amount: 100 });
} catch (error) {
if (error instanceof ZendFiError) {
// Handle ZendFi-specific errors
console.error('ZendFi error:', error.code, error.message);
if (error.type === 'authentication_error') {
console.error('API key is invalid');
} else if (error.type === 'rate_limit_error') {
console.error('Rate limit exceeded');
} else if (error.type === 'payment_error') {
console.error('Payment failed:', error.message);
}
// Log suggestion if available
if (error.suggestion) {
console.log('Suggestion:', error.suggestion);
}
} else {
// Handle other errors (network, etc.)
console.error('Unexpected error:', error);
}
}
Implement Retry Logic
The SDK has built-in retry logic:
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient({
retries: 3, // Automatically retry failed requests (5xx errors)
timeout: 30000 // 30 second timeout
});
// SDK will automatically retry on 5xx errors with exponential backoff
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD',
});
Or implement custom retry logic:
async function createPaymentWithRetry(params: any, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await zendfi.createPayment(params);
} catch (error) {
if (i === maxRetries - 1) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Graceful Degradation
export async function POST(request: Request) {
try {
const payment = await zendfi.createPayment({ amount: 100 });
return Response.json({ payment_url: payment.payment_url });
} catch (error) {
// Log error for investigation
console.error('Payment creation failed:', error);
// Provide fallback option
return Response.json({
error: 'Payment system temporarily unavailable',
fallback: {
method: 'email',
contact: 'payments@yourapp.com',
message: 'Please email us to complete your purchase'
}
}, { status: 503 });
}
}
Performance
Cache Responses
Cache data that doesn't change frequently:
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient();
let cachedPlan: any | null = null;
let cacheTime = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async function getSubscriptionPlan(planId: string) {
const now = Date.now();
// Return cached data if still valid
if (cachedPlan && (now - cacheTime) < CACHE_DURATION) {
return cachedPlan;
}
// Fetch fresh data
cachedPlan = await zendfi.getSubscriptionPlan(planId);
cacheTime = now;
return cachedPlan;
}
Use Pagination
For large datasets, always use pagination:
// Don't fetch everything at once
const allPayments = await zendfi.listPayments({ limit: 10000 });
// Paginate through results
async function getAllPayments() {
const payments = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const result = await zendfi.listPayments({
limit: 100,
page
});
payments.push(...result.data);
hasMore = result.has_more;
page++;
}
return payments;
}
Batch Operations
Group operations when possible:
// Slow: Individual requests
for (const order of orders) {
await zendfi.createPayment({ amount: order.total });
}
// Fast: Create payment links in batch
const links = await Promise.all(
orders.map(order =>
zendfi.createPaymentLink({
amount: order.total,
metadata: { order_id: order.id }
})
)
);
Optimize Webhook Processing
Return 200 immediately, process asynchronously:
import { verifyNextWebhook } from '@zendfi/sdk/webhooks';
export async function POST(request: Request) {
// Verify webhook
const event = await verifyNextWebhook(request);
if (!event) {
return new Response('Invalid signature', { status: 401 });
}
// Return 200 immediately
const response = new Response('OK', { status: 200 });
// Process asynchronously (don't await)
processWebhookAsync(event).catch(error => {
console.error('Webhook processing failed:', error);
});
return response;
}
async function processWebhookAsync(event: any) {
// Heavy processing here
if (event.event === 'payment.confirmed') {
await updateDatabase(event.data);
await sendConfirmationEmail(event.data);
await updateInventory(event.data);
}
}
Use Appropriate Timeouts
Set realistic timeouts based on operation:
// Fast operations (retrieve existing data)
const quickClient = new ZendFiClient({
timeout: 10000 // 10 seconds
});
// Standard operations (create, process payments)
const standardClient = new ZendFiClient({
timeout: 30000 // 30 seconds (default)
});
// Slow operations (reports, bulk operations)
const slowClient = new ZendFiClient({
timeout: 60000 // 60 seconds
});
Scalability
Use Idempotency
The SDK automatically handles idempotency with headers when enabled:
const zendfi = new ZendFiClient({
idempotencyEnabled: true // Automatically adds Idempotency-Key header
});
// SDK will add unique Idempotency-Key header automatically
// If request is retried, same key = same response (no duplicate payment)
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD',
});
Or manually set idempotency key via headers in your HTTP client.
Implement Database Transactions
import { db } from './database';
// Start transaction
const transaction = await db.transaction();
try {
// Create order in your database
const order = await transaction.orders.create({
user_id: userId,
total: 100,
status: 'pending'
});
// Create payment with ZendFi
const payment = await zendfi.createPayment({
amount: order.total,
currency: 'USD',
metadata: {
order_id: order.id
}
});
// Update order with payment ID
await transaction.orders.update(order.id, {
payment_id: payment.id
});
// Commit transaction
await transaction.commit();
return { order, payment };
} catch (error) {
// Rollback on error
await transaction.rollback();
throw error;
}
Separate Environments
Use different API keys for each environment:
# .env.development
ZENDFI_API_KEY=zfi_test_dev_abc...
ZENDFI_WEBHOOK_SECRET=whsec_test_dev_xyz...
# .env.staging
ZENDFI_API_KEY=zfi_test_staging_abc...
ZENDFI_WEBHOOK_SECRET=whsec_test_staging_xyz...
# .env.production
ZENDFI_API_KEY=zfi_live_prod_abc...
ZENDFI_WEBHOOK_SECRET=whsec_live_prod_xyz...
Monitor Performance
Track SDK performance metrics:
const startTime = Date.now();
try {
const payment = await zendfi.createPayment({ amount: 100 });
const duration = Date.now() - startTime;
console.log('Payment creation took:', duration, 'ms');
// Send to monitoring service
metrics.histogram('zendfi.payment.create.duration', duration);
metrics.increment('zendfi.payment.create.success');
} catch (error) {
metrics.increment('zendfi.payment.create.failure');
throw error;
}
Use Background Jobs
For non-urgent operations, use background jobs:
import { Queue } from 'bull';
const paymentQueue = new Queue('payments', {
redis: { host: 'localhost', port: 6379 }
});
// Add job to queue (fast)
await paymentQueue.add({
amount: 100,
userId: 'user_123',
orderId: 'order_456'
});
// Process jobs in background
paymentQueue.process(async (job) => {
const { amount, userId, orderId } = job.data;
const payment = await zendfi.createPayment({
amount,
currency: 'USD',
metadata: { user_id: userId, order_id: orderId }
});
return payment;
});
Code Organization
Create Reusable Services
// services/payment.service.ts
import { ZendFiClient } from '@zendfi/sdk';
export class PaymentService {
private zendfi: ZendFiClient;
constructor() {
this.zendfi = new ZendFiClient();
}
async createPayment(params: {
amount: number;
description: string;
metadata?: Record<string, any>;
}) {
return await this.zendfi.createPayment({
amount: params.amount,
currency: 'USD',
description: params.description,
metadata: params.metadata
});
}
async getPayment(paymentId: string) {
return await this.zendfi.getPayment(paymentId);
}
async listPayments(filters?: { status?: string; limit?: number }) {
return await this.zendfi.listPayments(filters);
}
}
// Use in your routes
import { PaymentService } from './services/payment.service';
const paymentService = new PaymentService();
export async function POST(request: Request) {
const { amount, description } = await request.json();
const payment = await paymentService.createPayment({
amount,
description
});
return Response.json({ payment_url: payment.payment_url });
}
Centralize Configuration
// config/zendfi.config.ts
import { ZendFiClient } from '@zendfi/sdk';
export const zendfiConfig = {
apiKey: process.env.ZENDFI_API_KEY!,
timeout: 30000,
retries: 3,
debug: process.env.NODE_ENV === 'development',
idempotencyEnabled: true,
};
export const zendfi = new ZendFiClient(zendfiConfig);
// Use throughout your app
import { zendfi } from './config/zendfi.config';
Type-Safe Metadata
// types/payment.types.ts
export interface OrderMetadata {
order_id: string;
user_id: string;
product_sku?: string;
coupon_code?: string;
}
// Use in your code
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD',
metadata: {
order_id: order.id,
user_id: user.id,
product_sku: product.sku
} satisfies OrderMetadata
});
Testing
Write Unit Tests
import { describe, it, expect, vi } from 'vitest';
import { PaymentService } from './payment.service';
describe('PaymentService', () => {
it('creates payment successfully', async () => {
const service = new PaymentService();
const payment = await service.createPayment({
amount: 100,
description: 'Test payment'
});
expect(payment.id).toMatch(/^pay_/);
expect(payment.amount).toBe(100);
});
it('handles payment creation failure', async () => {
// Mock ZendFi to throw error
const service = new PaymentService();
vi.spyOn(service, 'createPayment').mockRejectedValue(
new Error('Payment failed')
);
await expect(
service.createPayment({ amount: 100, description: 'Test' })
).rejects.toThrow('Payment failed');
});
});
Test Webhook Handlers
import { describe, it, expect } from 'vitest';
import { POST } from './api/webhooks/route';
describe('Webhook Handler', () => {
it('processes payment confirmed event', async () => {
const mockRequest = new Request('http://localhost/api/webhooks', {
method: 'POST',
headers: {
'x-zendfi-signature': 'mock_signature',
'content-type': 'application/json'
},
body: JSON.stringify({
event: 'payment.confirmed',
timestamp: new Date().toISOString(),
merchant_id: 'merch_123',
data: { id: 'pay_123', amount: 100 }
})
});
const response = await POST(mockRequest);
expect(response.status).toBe(200);
});
});
Monitoring & Logging
Structured Logging
import pino from 'pino';
const logger = pino();
const payment = await zendfi.createPayment({
amount: 100,
currency: 'USD'
});
logger.info({
event: 'payment.created',
payment_id: payment.id,
amount: payment.amount,
status: payment.status,
timestamp: new Date().toISOString()
});
Enable Debug Mode
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient({
debug: true // Logs all requests and responses
});
// Will log:
// [ZendFi] POST /api/v1/payments
// [ZendFi] Request: { amount: 100, currency: 'USD' }
// [ZendFi] ✓ 200 OK (342ms)
// [ZendFi] Response: { id: 'pay_123', ... }
Track Business Metrics
import { ZendFiClient } from '@zendfi/sdk';
const zendfi = new ZendFiClient();
// Track payment volume
async function trackPaymentMetrics() {
const payments = await zendfi.listPayments({
status: 'confirmed',
limit: 100
});
const totalRevenue = payments.data.reduce((sum, p) => sum + p.amount, 0);
const averagePayment = totalRevenue / payments.data.length;
// Send to analytics
analytics.track('revenue_metrics', {
total_revenue: totalRevenue,
payment_count: payments.data.length,
average_payment: averagePayment,
timestamp: new Date()
});
}
Deployment
Pre-Deployment Checklist
- Environment variables configured
- API keys rotated from test to production
- Webhook endpoints configured and verified
- Error tracking set up (Sentry, Datadog, etc.)
- Rate limiting implemented
- Logging configured
- Database migrations applied
- SSL/TLS certificates valid
- Load testing completed
- Backup and recovery plan in place
Zero-Downtime Deployments
// Use health checks to verify service is ready
export async function GET() {
try {
// Verify ZendFi connection
const zendfi = new ZendFiClient();
await zendfi.listPayments({ limit: 1 });
return Response.json({ status: 'healthy' });
} catch (error) {
return Response.json(
{ status: 'unhealthy', error: error.message },
{ status: 503 }
);
}
}
Next Steps
Learn More:
- Testing & Debugging - Debug common issues
- Next.js Integration - Framework-specific guide
- Express Integration - REST API guide
Need Help?