Best Practices
This guide covers production best practices for integrating with ezylegal.
Security
API Key Management
-
Never expose keys in client-side code
// Wrong - exposed in browser fetch('/api', { headers: { 'Authorization': 'Bearer ezc_live_...' }}); // Correct - call your server fetch('/api/create-session', { method: 'POST' }); -
Use environment variables
# .env (never commit) EZYCOLLECT_API_KEY=ezc_live_your_key -
Rotate keys regularly - Every 90 days or immediately if compromised
-
Use separate keys for environments
- Development: Test key
- Staging: Test key
- Production: Live key
Webhook Security
-
Always verify signatures - See Webhook Security
-
Use HTTPS - Required for production webhooks
-
Check timestamps - Reject webhooks older than 5 minutes
-
Validate event data - Don't trust input blindly
function validateSessionId(sessionId) {
// Verify format
if (!sessionId.startsWith('sess_')) {
throw new Error('Invalid session ID format');
}
// Verify it exists in your database
const session = await db.sessions.findByExternalId(sessionId);
if (!session) {
throw new Error('Unknown session');
}
return session;
}
Data Protection
- Encrypt sensitive data at rest
- Use TLS for all API calls (enforced by ezylegal)
- Implement access controls - Limit who can view form data
- Log access - Audit who accesses sensitive data
Reliability
Handle Network Failures
async function resilientApiCall(fn, options = {}) {
const { maxRetries = 3, baseDelay = 1000 } = options;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isRetryable =
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT' ||
error.status >= 500;
if (!isRetryable || attempt === maxRetries - 1) {
throw error;
}
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(r => setTimeout(r, delay));
}
}
}
Idempotent Operations
Always use idempotency keys for session creation:
async function createSession(data) {
const idempotencyKey = `session-${data.caseId}-${Date.now()}`;
return fetch('https://api.ezy-forms.com.au/v1/sessions', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey,
// ...
},
body: JSON.stringify(data),
});
}
Graceful Degradation
async function getSessionStatus(sessionId) {
try {
// Try API first
const response = await fetchWithTimeout(
`https://api.ezy-forms.com.au/v1/sessions/${sessionId}`,
{ timeout: 5000 }
);
return response.json();
} catch (error) {
// Fall back to cached data
const cached = await cache.get(`session:${sessionId}`);
if (cached) {
return { ...cached, fromCache: true };
}
// Return minimal info from database
const local = await db.sessions.findByExternalId(sessionId);
return {
id: sessionId,
status: local.status,
fromDatabase: true,
};
}
}
Performance
Batch Operations
If you need to check many sessions, batch your requests:
async function checkMultipleSessions(sessionIds) {
// Limit concurrent requests
const limit = pLimit(5); // 5 concurrent
const results = await Promise.all(
sessionIds.map(id =>
limit(() => getSessionStatus(id))
)
);
return results;
}
Caching
Cache read-heavy data:
const sessionCache = new LRU({
max: 1000,
ttl: 60 * 1000, // 1 minute
});
async function getSession(sessionId) {
const cached = sessionCache.get(sessionId);
if (cached) return cached;
const session = await fetchSession(sessionId);
sessionCache.set(sessionId, session);
return session;
}
Async Webhook Processing
Never block webhook responses:
// Wrong - slow response
app.post('/webhook', async (req, res) => {
await processWebhook(req.body); // Could take seconds
res.send('OK');
});
// Correct - respond quickly
app.post('/webhook', async (req, res) => {
await queue.add('webhook', req.body); // Milliseconds
res.send('OK');
});
Monitoring
Essential Metrics
Track these metrics:
| Metric | Description |
|--------|-------------|
| session.created | Sessions created per minute |
| session.completed | Completions per minute |
| webhook.received | Webhooks received per minute |
| webhook.processed | Webhooks successfully processed |
| webhook.failed | Failed webhook processing |
| api.latency | API response time |
| api.errors | API error rate |
Alerting
Set up alerts for:
- Webhook failures spike - More than 5 failures in 5 minutes
- Completion rate drop - Below 70% completion rate
- API errors - Error rate above 1%
- Processing backlog - Queue depth above 100
Logging
Log important events with context:
logger.info('Session created', {
sessionId: session.id,
customerId: customer.id,
formType: 'financial_statement',
timestamp: new Date().toISOString(),
});
logger.error('Webhook processing failed', {
webhookId,
error: error.message,
stack: error.stack,
attempt: retryCount,
});
User Experience
Clear Communication
- Set expectations - Tell customers how long the form takes
- Explain the process - What happens after they complete it
- Provide support - How to get help if stuck
Session Management
// Check if session exists before creating new one
async function getOrCreateSession(customerId, caseId) {
// Look for active session
const existing = await db.sessions.findOne({
customerId,
caseId,
status: { $in: ['pending', 'in_progress'] },
expiresAt: { $gt: new Date() },
});
if (existing) {
return existing; // Reuse existing session
}
// Create new session
return createSession({ customerId, caseId });
}
Progress Tracking
Show customers their progress:
async function getSessionProgress(sessionId) {
const session = await getSession(sessionId);
return {
status: session.status,
sectionsCompleted: session.progress?.sections_completed || 0,
sectionsTotal: session.progress?.sections_total || 7,
percentage: session.progress?.percentage || 0,
estimatedTimeRemaining: calculateRemainingTime(session),
};
}
Compliance
Data Retention
- Define retention policies - How long to keep form data
- Implement deletion - Remove data when no longer needed
- Document your practices - For compliance audits
Privacy
- Minimize data collection - Only request what you need
- Get consent - Before collecting financial information
- Secure storage - Encrypt sensitive data
Audit Trail
Log all access to sensitive data:
async function accessFormData(sessionId, userId) {
const data = await getFormData(sessionId);
await auditLog.create({
action: 'form_data_accessed',
sessionId,
userId,
timestamp: new Date(),
ipAddress: req.ip,
});
return data;
}
Common Pitfalls
1. Not Handling Duplicates
Webhooks can be delivered multiple times. Always check:
const existing = await db.webhookEvents.findOne({ webhookId });
if (existing) return; // Skip duplicate
2. Blocking on Webhook Response
Process webhooks asynchronously to avoid timeouts.
3. Not Validating Signatures
Always verify webhook signatures in production.
4. Hardcoding Configuration
Use environment variables for all configuration.
5. Ignoring Rate Limits
Implement backoff when rate limited.
Related Guides
- Integration Guide - Complete integration walkthrough
- Webhook Security - Signature verification
- Error Handling - Error codes and handling