Best Practices
Recommended practices for integrating with the Courimax API.
Authentication
Secure API Keys
- Store API keys in environment variables or secret management systems
- Never commit API keys to version control
- Use different keys for development, staging, and production
- Rotate keys regularly (every 90 days recommended)
// Good
const apiKey = process.env.COURIMAX_API_KEY;
// Bad
const apiKey = 'cmx_hardcoded_key';
Key Rotation
Rotate API keys without downtime:
- Create new API key
- Update all systems to use new key
- Verify new key works in production
- Revoke old API key
# Create new key
curl -X POST "/provisioning/organizations/{orgId}/api-keys" \
-H "X-Provisioning-Secret: your_secret" \
-d '{"name": "Production Key v2", "platform": "yango"}'
# Update systems with new key
# ...
# Revoke old key
curl -X DELETE "/provisioning/organizations/{orgId}/api-keys/{oldKeyId}" \
-H "X-Provisioning-Secret: your_secret"
Error Handling
Implement Retry Logic
Use exponential backoff for transient errors:
async function requestWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
// Retry on rate limit or server errors
if (response.status === 429 || response.status >= 500) {
const waitTime = Math.pow(2, i) * 1000;
await sleep(waitTime);
continue;
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000);
}
}
}
Handle All Error Types
async function createOrder(orderData) {
try {
const response = await fetch('/integrations/yango/orders', {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify(orderData)
});
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 400:
throw new ValidationError(error.message);
case 401:
throw new AuthenticationError('Invalid API key');
case 404:
throw new NotFoundError('Order not found');
case 409:
throw new ConflictError('Order already exists');
case 429:
throw new RateLimitError('Rate limit exceeded');
default:
throw new ApiError(error.message);
}
}
return await response.json();
} catch (error) {
logger.error('Order creation failed', error);
throw error;
}
}
Idempotency
Use Idempotency Keys
Always include idempotency keys for mutating operations:
const { v4: uuidv4 } = require('uuid');
async function createOrderWithIdempotency(orderData) {
const idempotencyKey = uuidv4();
const response = await fetch('/integrations/yango/orders', {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
return response.json();
}
Store Idempotency Keys
Store idempotency keys to safely retry requests:
class OrderService {
async createOrder(orderData) {
const idempotencyKey = orderData.idempotencyKey || uuidv4();
// Check if we already made this request
const existing = await this.db.requests.findOne({
where: { idempotencyKey }
});
if (existing) {
return existing.response;
}
// Make the request
const response = await this.api.createOrder(orderData, idempotencyKey);
// Store the result
await this.db.requests.create({
idempotencyKey,
response
});
return response;
}
}
Performance
Use Bulk Endpoints
Prefer bulk endpoints over multiple individual requests:
// Bad: Multiple requests
for (const date of dates) {
await fetch(`/delivery-windows?zone=${zone}&date=${date}`);
}
// Good: Single bulk request
await fetch(`/availability-bulk?fromDate=${startDate}&toDate=${endDate}`);
Implement Caching
Cache responses when appropriate:
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5 minutes
async function getDeliveryWindows(zone, date) {
const cacheKey = `windows-${zone}-${date}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const response = await fetch(
`/delivery-windows?zone=${zone}&date=${date}`,
{ headers: { 'X-API-Key': API_KEY } }
);
const data = await response.json();
cache.set(cacheKey, data);
return data;
}
Batch Webhook Processing
Process webhooks asynchronously:
app.post('/webhooks/courimax', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
try {
await webhookQueue.add(req.body);
} catch (error) {
logger.error('Failed to queue webhook', error);
}
});
Webhooks
Verify Signatures
Always verify webhook signatures:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
app.post('/webhooks/courimax', (req, res) => {
const signature = req.headers['x-courimax-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
handleWebhook(req.body);
res.status(200).send('OK');
});
Handle Idempotency
Webhooks may be delivered multiple times:
const processedEvents = new Set();
app.post('/webhooks/courimax', (req, res) => {
const eventId = req.body.id;
if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}
processedEvents.add(eventId);
handleWebhook(req.body);
res.status(200).send('OK');
});
Respond Quickly
Return 200 OK immediately and process asynchronously:
app.post('/webhooks/courimax', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
setImmediate(() => {
processWebhookEvent(req.body).catch(error => {
logger.error('Webhook processing failed', error);
});
});
});
Data Validation
Validate Coordinates
Ensure coordinates are valid before sending:
function validateCoordinates(lat, lng) {
if (lat < -90 || lat > 90) {
throw new Error('Invalid latitude');
}
if (lng < -180 || lng > 180) {
throw new Error('Invalid longitude');
}
}
function createOrder(orderData) {
validateCoordinates(orderData.pickupLat, orderData.pickupLng);
validateCoordinates(orderData.dropoffLat, orderData.dropoffLng);
return api.createOrder(orderData);
}
Validate Phone Numbers
Use E.164 format for phone numbers:
function validatePhone(phone) {
const e164Regex = /^\+[1-9]\d{1,14}$/;
if (!e164Regex.test(phone)) {
throw new Error('Phone must be in E.164 format');
}
}
Validate Dates
Use ISO 8601 format for dates:
function validateDate(date) {
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
if (!isoRegex.test(date)) {
throw new Error('Date must be in ISO 8601 format');
}
}
Testing
Use Sandbox Environment
Test in sandbox before production:
const API_URL = process.env.NODE_ENV === 'production'
? 'https://api.courimax.com/api'
: 'https://api-sandbox.courimax.com/api';
Mock Webhooks
Test webhook handling locally:
// Test webhook handler
const testEvent = {
id: 'test-event-id',
event: 'order.delivered',
payload: {
id: 'order-uuid',
status: 'delivered',
externalRef: 'TEST-001'
},
at: new Date().toISOString()
};
await webhookHandler(testEvent);
Load Testing
Test your integration under load:
async function loadTest(concurrency, duration) {
const startTime = Date.now();
const requests = [];
while (Date.now() - startTime < duration) {
while (requests.length < concurrency) {
requests.push(createTestOrder());
}
await Promise.race(requests);
requests.splice(requests.findIndex(r => r.isResolved), 1);
}
}
Monitoring
Log API Calls
Log all API calls for debugging:
class ApiClient {
async request(method, url, data) {
const startTime = Date.now();
try {
const response = await fetch(url, {
method,
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
const duration = Date.now() - startTime;
logger.info('API request', {
method,
url,
status: response.status,
duration
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
logger.error('API request failed', {
method,
url,
error: error.message,
duration
});
throw error;
}
}
}
Monitor Webhook Delivery
Track webhook delivery success:
class WebhookMonitor {
constructor() {
this.stats = {
received: 0,
processed: 0,
failed: 0
};
}
trackReceived() {
this.stats.received++;
}
trackProcessed() {
this.stats.processed++;
}
trackFailed(error) {
this.stats.failed++;
logger.error('Webhook processing failed', error);
}
getStats() {
return {
...this.stats,
successRate: this.stats.processed / this.stats.received
};
}
}
Set Up Alerts
Alert on critical issues:
function checkHealth() {
const stats = webhookMonitor.getStats();
if (stats.successRate < 0.95) {
sendAlert({
type: 'webhook_success_rate_low',
successRate: stats.successRate
});
}
if (stats.failed > 10) {
sendAlert({
type: 'webhook_failures_high',
failed: stats.failed
});
}
}
// Check every 5 minutes
setInterval(checkHealth, 5 * 60 * 1000);
Security
HTTPS Only
Always use HTTPS for API calls and webhook endpoints.
Validate All Input
Validate and sanitize all input data:
const Joi = require('joi');
const orderSchema = Joi.object({
externalRef: Joi.string().required(),
pickupAddress: Joi.string().required(),
dropoffAddress: Joi.string().required(),
recipientName: Joi.string().required(),
recipientPhone: Joi.string().pattern(/^\+[1-9]\d{1,14}$/).required()
});
function createOrder(orderData) {
const { error } = orderSchema.validate(orderData);
if (error) {
throw new ValidationError(error.message);
}
return api.createOrder(orderData);
}
Use Environment Variables
Store configuration in environment variables:
const config = {
apiUrl: process.env.COURIMAX_API_URL,
apiKey: process.env.COURIMAX_API_KEY,
webhookSecret: process.env.COURIMAX_WEBHOOK_SECRET
};
Next Steps
- Error Codes - Complete error reference
- Rate Limits - Rate limit information
- Integration Guides - Platform-specific guides