Skip to main content

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:

  1. Create new API key
  2. Update all systems to use new key
  3. Verify new key works in production
  4. 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