Best Practices

This guide covers production best practices for integrating with ezylegal.

Security

API Key Management

  1. 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' });
    
  2. Use environment variables

    # .env (never commit)
    EZYCOLLECT_API_KEY=ezc_live_your_key
    
  3. Rotate keys regularly - Every 90 days or immediately if compromised

  4. Use separate keys for environments

    • Development: Test key
    • Staging: Test key
    • Production: Live key

Webhook Security

  1. Always verify signatures - See Webhook Security

  2. Use HTTPS - Required for production webhooks

  3. Check timestamps - Reject webhooks older than 5 minutes

  4. 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

  1. Encrypt sensitive data at rest
  2. Use TLS for all API calls (enforced by ezylegal)
  3. Implement access controls - Limit who can view form data
  4. 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:

  1. Webhook failures spike - More than 5 failures in 5 minutes
  2. Completion rate drop - Below 70% completion rate
  3. API errors - Error rate above 1%
  4. 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

  1. Set expectations - Tell customers how long the form takes
  2. Explain the process - What happens after they complete it
  3. 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

  1. Define retention policies - How long to keep form data
  2. Implement deletion - Remove data when no longer needed
  3. Document your practices - For compliance audits

Privacy

  1. Minimize data collection - Only request what you need
  2. Get consent - Before collecting financial information
  3. 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