Skip to content
Last updated

Best Practices

Building a production-ready integration with the Sprinklr API requires more than just making successful API calls. This guide shares battle-tested patterns, common pitfalls to avoid, and techniques that separate hobby projects from enterprise-grade integrations.

Why best practices matter

The difference between a working integration and a reliable one often comes down to how you handle the details:

  • A working integration makes API calls and gets data back
  • A reliable integration handles errors gracefully, respects rate limits, protects credentials, scales efficiently, and degrades gracefully under load

Following these best practices will help you build integrations that:

  • Don't wake you up at 3 AM with production incidents
  • Scale from 10 requests/day to 10,000 without rewriting
  • Handle errors without losing data
  • Perform efficiently and stay under rate limits
  • Are maintainable by your team (and future you)

Let's dive into the patterns that make this possible.

Authentication & security

Security isn't optional—it's fundamental. A single exposed credential can compromise your entire integration and your customers' data.

Store credentials securely

❌ Never do this:

// DANGER: Credentials in code
const accessToken = "SPRD_tjACKHVlwb3D7Xd2WDZI3VxgBnI3Lmk2mwFfZasVpkDNzZSRzRp6lbHpxLqOCJ8Gomp6iYXkl4JGLr91cpUxfw==";
const apiKey = "ebgw8cwygvgc7vptgxty2ekq";

// This ends up in:
// - Your git repository (forever)
// - Your build artifacts
// - Your error logs
// - Anyone who can access your code

✅ Do this instead:

Node.js:

// Use environment variables
const accessToken = process.env.SPRINKLR_ACCESS_TOKEN;
const apiKey = process.env.SPRINKLR_API_KEY;

if (!accessToken || !apiKey) {
  throw new Error('Missing required credentials. Set SPRINKLR_ACCESS_TOKEN and SPRINKLR_API_KEY environment variables.');
}

// Or use a secrets manager (production)
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

async function getCredentials() {
  const secret = await secretsManager.getSecretValue({
    SecretId: 'sprinklr/api/credentials'
  }).promise();

  return JSON.parse(secret.SecretString);
}

Python:

import os
from typing import Dict

# Use environment variables
access_token = os.environ.get('SPRINKLR_ACCESS_TOKEN')
api_key = os.environ.get('SPRINKLR_API_KEY')

if not access_token or not api_key:
    raise ValueError('Missing required credentials')

# Or use a secrets manager
import boto3

def get_credentials() -> Dict[str, str]:
    client = boto3.client('secretsmanager')
    secret = client.get_secret_value(SecretId='sprinklr/api/credentials')
    return json.loads(secret['SecretString'])

PHP:

<?php
// Use environment variables
$accessToken = getenv('SPRINKLR_ACCESS_TOKEN');
$apiKey = getenv('SPRINKLR_API_KEY');

if (!$accessToken || !$apiKey) {
    throw new Exception('Missing required credentials');
}

// Or use a secrets manager
use Aws\SecretsManager\SecretsManagerClient;

function getCredentials() {
    $client = new SecretsManagerClient([
        'region' => 'us-east-1',
        'version' => 'latest'
    ]);

    $result = $client->getSecretValue([
        'SecretId' => 'sprinklr/api/credentials'
    ]);

    return json_decode($result['SecretString'], true);
}
?>

Java:

import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;

public class CredentialManager {
    public static Credentials getCredentials() {
        // Use environment variables
        String accessToken = System.getenv("SPRINKLR_ACCESS_TOKEN");
        String apiKey = System.getenv("SPRINKLR_API_KEY");

        if (accessToken == null || apiKey == null) {
            throw new IllegalStateException("Missing required credentials");
        }

        return new Credentials(accessToken, apiKey);
    }

    // Or use secrets manager
    public static Credentials getCredentialsFromSecretsManager() {
        SecretsManagerClient client = SecretsManagerClient.create();

        GetSecretValueRequest request = GetSecretValueRequest.builder()
            .secretId("sprinklr/api/credentials")
            .build();

        String secret = client.getSecretValue(request).secretString();
        return parseCredentials(secret);
    }
}

Separate credentials by environment

Use different credentials for development, staging, and production:

// config.js
module.exports = {
  development: {
    apiUrl: 'https://qa-api-v3.sprinklr.com',
    accessToken: process.env.DEV_SPRINKLR_ACCESS_TOKEN,
    apiKey: process.env.DEV_SPRINKLR_API_KEY
  },
  production: {
    apiUrl: 'https://api-v3.sprinklr.com',
    accessToken: process.env.PROD_SPRINKLR_ACCESS_TOKEN,
    apiKey: process.env.PROD_SPRINKLR_API_KEY
  }
};

const config = module.exports[process.env.NODE_ENV || 'development'];

Why this matters:

  • Development bugs don't affect production data
  • You can test freely without worry
  • Compromised dev credentials don't expose production
  • You can revoke environment-specific credentials independently

Rotate credentials regularly

Implement a credential rotation schedule:

class CredentialManager {
  constructor() {
    this.currentCredentials = null;
    this.credentialAge = null;
    this.rotationWarningDays = 80; // Warn before 90-day expiry
  }

  async getCredentials() {
    if (!this.currentCredentials) {
      this.currentCredentials = await this.loadCredentials();
      this.credentialAge = await this.getCredentialAge();
    }

    // Check if credentials need rotation
    if (this.credentialAge > this.rotationWarningDays) {
      console.warn(`⚠️ Credentials are ${this.credentialAge} days old. Consider rotating soon.`);
      this.alertOps('Credential rotation needed');
    }

    return this.currentCredentials;
  }

  async rotateCredentials(newAccessToken, newApiKey) {
    // Implement gradual rotation with fallback
    const oldCredentials = this.currentCredentials;

    try {
      // Test new credentials
      await this.testCredentials(newAccessToken, newApiKey);

      // Switch to new credentials
      this.currentCredentials = {
        accessToken: newAccessToken,
        apiKey: newApiKey
      };

      // Keep old credentials for 24 hours as fallback
      setTimeout(() => {
        console.log('Old credentials expired from fallback');
      }, 86400000);

      console.log('✅ Credentials rotated successfully');
    } catch (error) {
      console.error('Credential rotation failed, keeping old credentials');
      throw error;
    }
  }

  async testCredentials(accessToken, apiKey) {
    // Make a test API call to verify credentials work
    const response = await fetch('https://api-v3.sprinklr.com/api/v3/account', {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Key': apiKey
      }
    });

    if (!response.ok) {
      throw new Error('Invalid credentials');
    }
  }
}

Always use HTTPS

Never make API calls over plain HTTP:

// ✅ Correct - HTTPS
const apiUrl = 'https://api-v3.sprinklr.com';

// ❌ Wrong - Plain HTTP (credentials visible in plain text!)
const apiUrl = 'http://api-v3.sprinklr.com';

// ✅ Enforce HTTPS in code
if (!apiUrl.startsWith('https://')) {
  throw new Error('API URL must use HTTPS');
}

Efficiency: Do more with less

Efficient integrations use fewer API requests, process faster, and stay well under rate limits.

Use batch operations religiously

This is the single most effective optimization you can make.

Impact comparison:

Task: Fetch 100 accounts

Without batching:
- 100 API requests
- ~30 seconds total time
- 100 of your rate limit quota
- More error opportunities

With batching:
- 2 API requests (2 batches of 50)
- ~600ms total time
- 2 of your rate limit quota
- 50x more efficient

Implementation:

Node.js:

// ❌ Inefficient
async function getAccountsOneByOne(accountIds) {
  const accounts = [];
  for (const id of accountIds) {
    const account = await fetch(
      `https://api-v3.sprinklr.com/api/v3/account?accountId=${id}`,
      { headers: { 'Authorization': `Bearer ${token}`, 'Key': key } }
    );
    accounts.push(await account.json());
  }
  return accounts;
}

// ✅ Efficient
async function getAccountsInBatches(accountIds) {
  const batchSize = 50;
  const batches = [];

  // Split into batches of 50
  for (let i = 0; i < accountIds.length; i += batchSize) {
    batches.push(accountIds.slice(i, i + batchSize));
  }

  // Fetch all batches
  const results = await Promise.all(
    batches.map(batch =>
      fetch('https://api-v3.sprinklr.com/api/v3/account/batch', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Key': key,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ accountIds: batch })
      }).then(r => r.json())
    )
  );

  // Flatten results
  return results.flatMap(r => r.data);
}

Cache intelligently

Not all data needs to be fresh every time.

Caching strategy by data type:

Data TypeRecommended TTLRationale
Account metadata30-60 minutesChanges infrequently
User profiles15-30 minutesMay update but not constantly
Messages/posts1-5 minutesMore dynamic
Cases1-2 minutesChanges as agents work
Configuration1-24 hoursRarely changes

Production-ready cache implementation:

class SmartCache {
  constructor() {
    this.cache = new Map();
    this.stats = { hits: 0, misses: 0, evictions: 0 };
  }

  set(key, value, ttlMs = 300000) {
    // Limit cache size
    if (this.cache.size > 10000) {
      // Evict oldest entry
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
      this.stats.evictions++;
    }

    this.cache.set(key, {
      value,
      expiresAt: Date.now() + ttlMs,
      createdAt: Date.now()
    });
  }

  get(key) {
    const entry = this.cache.get(key);

    if (!entry) {
      this.stats.misses++;
      return null;
    }

    // Check expiry
    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key);
      this.stats.misses++;
      return null;
    }

    this.stats.hits++;
    return entry.value;
  }

  getStats() {
    const total = this.stats.hits + this.stats.misses;
    const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(2) : 0;

    return {
      ...this.stats,
      total,
      hitRate: `${hitRate}%`,
      size: this.cache.size
    };
  }

  clear() {
    this.cache.clear();
  }

  // Cache with automatic revalidation
  async getOrFetch(key, fetchFn, ttl = 300000) {
    const cached = this.get(key);
    if (cached !== null) {
      return cached;
    }

    const value = await fetchFn();
    this.set(key, value, ttl);
    return value;
  }
}

// Usage
const cache = new SmartCache();

async function getAccount(accountId) {
  return cache.getOrFetch(
    `account:${accountId}`,
    async () => {
      const response = await fetch(`https://api-v3.sprinklr.com/api/v3/account?accountId=${accountId}`, {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Key': apiKey
        }
      });
      return response.json();
    },
    1800000 // 30 minutes
  );
}

// Monitor cache performance
setInterval(() => {
  console.log('Cache stats:', cache.getStats());
  // Output: { hits: 847, misses: 153, evictions: 2, total: 1000, hitRate: '84.70%', size: 892 }
}, 60000);

Implement request queuing

Prevent overwhelming the API with concurrent requests:

class RequestQueue {
  constructor(options = {}) {
    this.concurrency = options.concurrency || 5;
    this.requestsPerMinute = options.requestsPerMinute || 60;
    this.queue = [];
    this.running = 0;
    this.requestTimes = [];
  }

  async add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.process();
    });
  }

  async process() {
    // Check concurrency limit
    if (this.running >= this.concurrency) {
      return;
    }

    // Check rate limit
    const now = Date.now();
    this.requestTimes = this.requestTimes.filter(t => t > now - 60000);

    if (this.requestTimes.length >= this.requestsPerMinute) {
      // Wait until oldest request is outside the 1-minute window
      const oldestRequest = this.requestTimes[0];
      const waitTime = 60000 - (now - oldestRequest);
      setTimeout(() => this.process(), waitTime);
      return;
    }

    // Get next request
    const item = this.queue.shift();
    if (!item) return;

    this.running++;
    this.requestTimes.push(now);

    try {
      const result = await item.fn();
      item.resolve(result);
    } catch (error) {
      item.reject(error);
    } finally {
      this.running--;
      // Process next request
      setImmediate(() => this.process());
    }
  }

  getQueueSize() {
    return this.queue.length;
  }
}

// Usage
const queue = new RequestQueue({
  concurrency: 5,           // Max 5 concurrent requests
  requestsPerMinute: 60     // Stay under rate limit
});

// All requests automatically queued and rate-limited
async function fetchAccount(accountId) {
  return queue.add(async () => {
    const response = await fetch(
      `https://api-v3.sprinklr.com/api/v3/account?accountId=${accountId}`,
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Key': apiKey
        }
      }
    );
    return response.json();
  });
}

Error handling: Fail gracefully

Production systems fail. The question is whether your integration fails gracefully or catastrophically.

Implement comprehensive retry logic

async function retryWithBackoff(fn, options = {}) {
  const {
    maxRetries = 5,
    baseDelay = 1000,
    maxDelay = 30000,
    retryableErrors = [408, 429, 500, 502, 503, 504]
  } = options;

  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // Check if error is retryable
      const statusCode = error.response?.status;
      if (!retryableErrors.includes(statusCode)) {
        throw error; // Don't retry non-retryable errors
      }

      // Don't retry on last attempt
      if (attempt === maxRetries - 1) {
        break;
      }

      // Calculate delay with exponential backoff + jitter
      const exponentialDelay = Math.min(
        baseDelay * Math.pow(2, attempt),
        maxDelay
      );
      const jitter = Math.random() * 1000; // Add randomness
      const delay = exponentialDelay + jitter;

      console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// Usage
const account = await retryWithBackoff(
  async () => {
    const response = await fetch(
      `https://api-v3.sprinklr.com/api/v3/account?accountId=${accountId}`,
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Key': apiKey
        }
      }
    );

    if (!response.ok) {
      const error = new Error(`HTTP ${response.status}`);
      error.response = response;
      throw error;
    }

    return response.json();
  },
  {
    maxRetries: 5,
    retryableErrors: [429, 500, 502, 503, 504]
  }
);

Log errors comprehensively

class ErrorLogger {
  static log(error, context = {}) {
    const errorInfo = {
      timestamp: new Date().toISOString(),
      message: error.message,
      stack: error.stack,
      ...context
    };

    // Log different details based on error type
    if (error.response) {
      errorInfo.status = error.response.status;
      errorInfo.statusText = error.response.statusText;
      errorInfo.headers = Object.fromEntries(error.response.headers);
    }

    // Log to console (development)
    console.error('API Error:', JSON.stringify(errorInfo, null, 2));

    // Send to error tracking service (production)
    if (process.env.NODE_ENV === 'production') {
      this.sendToErrorTracker(errorInfo);
    }

    return errorInfo;
  }

  static sendToErrorTracker(errorInfo) {
    // Send to Sentry, Datadog, CloudWatch, etc.
    // Example with hypothetical service:
    errorTracker.captureException(errorInfo);
  }
}

// Usage
try {
  const response = await makeAPICall();
} catch (error) {
  ErrorLogger.log(error, {
    operation: 'fetchAccount',
    accountId: '600057112',
    userId: currentUser.id,
    environment: process.env.NODE_ENV
  });

  // Rethrow or handle
  throw error;
}

Provide user-friendly error messages

Never expose raw API errors to end users:

class UserFriendlyError extends Error {
  constructor(apiError, userMessage) {
    super(userMessage);
    this.apiError = apiError;
    this.userMessage = userMessage;
  }

  static fromAPIError(apiError, context = {}) {
    const status = apiError.response?.status;
    const code = apiError.code;

    let userMessage;

    switch (code) {
      case 'INVALID_PARAMETER':
        userMessage = 'The information provided is invalid. Please check and try again.';
        break;
      case 'NOT_FOUND':
        userMessage = `The ${context.resourceType || 'item'} you're looking for doesn't exist.`;
        break;
      case 'UNAUTHORIZED':
        userMessage = 'Your session has expired. Please log in again.';
        break;
      case 'FORBIDDEN':
        userMessage = "You don't have permission to access this resource.";
        break;
      case 'RATE_LIMIT_EXCEEDED':
        userMessage = 'Too many requests. Please wait a moment and try again.';
        break;
      case 'VALIDATION_ERROR':
        userMessage = 'Some required information is missing or incorrect.';
        break;
      default:
        if (status >= 500) {
          userMessage = 'Our service is temporarily unavailable. Please try again in a few minutes.';
        } else {
          userMessage = 'Something went wrong. Please try again.';
        }
    }

    return new UserFriendlyError(apiError, userMessage);
  }
}

// Usage in your application
try {
  const account = await getAccount(accountId);
} catch (error) {
  const friendlyError = UserFriendlyError.fromAPIError(error, {
    resourceType: 'account'
  });

  // Show to user
  showErrorToUser(friendlyError.userMessage);

  // Log technical details
  console.error(friendlyError.apiError);
}

Rate limiting: Stay under the radar

Respecting rate limits isn't just polite—it's essential for a reliable integration.

Monitor your usage proactively

class RateLimitMonitor {
  constructor(alertThreshold = 0.8) {
    this.alertThreshold = alertThreshold;
    this.requests = [];
  }

  logRequest(response) {
    const limit = parseInt(response.headers.get('X-RateLimit-Limit'));
    const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
    const reset = parseInt(response.headers.get('X-RateLimit-Reset'));

    this.requests.push({
      timestamp: Date.now(),
      limit,
      remaining,
      reset
    });

    // Alert if approaching limit
    const usagePercentage = 1 - (remaining / limit);
    if (usagePercentage > this.alertThreshold) {
      this.alert({
        message: `Rate limit warning: ${Math.round(usagePercentage * 100)}% used`,
        remaining,
        limit,
        resetAt: new Date(reset * 1000)
      });
    }

    return { limit, remaining, reset };
  }

  alert(info) {
    console.warn('⚠️ RATE LIMIT WARNING:', info);
    // Send to monitoring system
    if (process.env.NODE_ENV === 'production') {
      this.notifyOpsTeam(info);
    }
  }

  getUsageStats() {
    const now = Date.now();
    const lastMinute = this.requests.filter(r => r.timestamp > now - 60000);
    const lastHour = this.requests.filter(r => r.timestamp > now - 3600000);

    return {
      lastMinute: {
        count: lastMinute.length,
        avgRemaining: lastMinute.reduce((sum, r) => sum + r.remaining, 0) / lastMinute.length
      },
      lastHour: {
        count: lastHour.length
      }
    };
  }
}

// Usage
const monitor = new RateLimitMonitor(0.8); // Alert at 80% usage

async function makeAPICall(endpoint) {
  const response = await fetch(endpoint, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Key': apiKey
    }
  });

  monitor.logRequest(response);

  return response.json();
}

Validation: Trust nothing

Validate both inputs and outputs to catch issues early.

Validate requests before sending

class RequestValidator {
  static validateAccountId(accountId) {
    if (!accountId) {
      throw new ValidationError('Account ID is required');
    }

    if (typeof accountId !== 'string') {
      throw new ValidationError('Account ID must be a string');
    }

    if (accountId.length < 1 || accountId.length > 100) {
      throw new ValidationError('Account ID must be 1-100 characters');
    }

    return true;
  }

  static validateSearchParams(params) {
    if (params.page !== undefined) {
      if (!Number.isInteger(params.page) || params.page < 0) {
        throw new ValidationError('Page must be a non-negative integer');
      }
    }

    if (params.size !== undefined) {
      if (!Number.isInteger(params.size) || params.size < 1 || params.size > 100) {
        throw new ValidationError('Size must be between 1 and 100');
      }
    }

    if (params.startDate && params.endDate) {
      const start = new Date(params.startDate);
      const end = new Date(params.endDate);

      if (end < start) {
        throw new ValidationError('End date must be after start date');
      }
    }

    return true;
  }
}

// Usage
async function searchCases(params) {
  RequestValidator.validateSearchParams(params);

  const response = await fetch('https://api-v3.sprinklr.com/api/v3/case/search', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Key': apiKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(params)
  });

  return response.json();
}

Validate responses before using

class ResponseValidator {
  static validate(response, expectedSchema) {
    if (!response || typeof response !== 'object') {
      throw new ValidationError('Invalid response format');
    }

    if (response.success === false) {
      if (!response.errors || !Array.isArray(response.errors)) {
        throw new ValidationError('Error response missing errors array');
      }
      // This is a valid error response
      return response;
    }

    if (response.success !== true) {
      throw new ValidationError('Response missing success field');
    }

    if (!response.data) {
      throw new ValidationError('Success response missing data field');
    }

    // Validate against expected schema
    if (expectedSchema) {
      this.validateSchema(response.data, expectedSchema);
    }

    return response;
  }

  static validateSchema(data, schema) {
    for (const [field, type] of Object.entries(schema)) {
      if (Array.isArray(data)) {
        // Validate each item in array
        data.forEach(item => {
          if (typeof item[field] !== type && item[field] !== null) {
            console.warn(`Field ${field} has unexpected type. Expected ${type}, got ${typeof item[field]}`);
          }
        });
      } else {
        if (typeof data[field] !== type && data[field] !== null) {
          console.warn(`Field ${field} has unexpected type. Expected ${type}, got ${typeof data[field]}`);
        }
      }
    }
  }
}

// Usage
const response = await fetch('https://api-v3.sprinklr.com/api/v3/account', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Key': apiKey
  }
});

const data = await response.json();

// Validate response structure
ResponseValidator.validate(data, {
  accountId: 'string',
  name: 'string',
  channelType: 'string',
  status: 'string'
});

Testing: Test like you mean it

Comprehensive testing catches issues before users do.

Test error scenarios explicitly

describe('Sprinklr API Client', () => {
  test('handles 404 errors gracefully', async () => {
    const client = new SprinklrClient(token, key);

    await expect(
      client.getAccount('nonexistent-id')
    ).rejects.toThrow('not found');
  });

  test('retries on 429 rate limit', async () => {
    const client = new SprinklrClient(token, key);

    // Mock API to return 429 then 200
    mockAPI
      .onGet('/account').replyOnce(429)
      .onGet('/account').replyOnce(200, { success: true, data: {} });

    const result = await client.getAccount('123');
    expect(result).toBeDefined();
  });

  test('handles network timeouts', async () => {
    const client = new SprinklrClient(token, key, { timeout: 100 });

    mockAPI.onGet('/account').timeout();

    await expect(
      client.getAccount('123')
    ).rejects.toThrow('timeout');
  });

  test('validates response structure', async () => {
    const client = new SprinklrClient(token, key);

    mockAPI.onGet('/account').reply(200, { invalid: 'response' });

    await expect(
      client.getAccount('123')
    ).rejects.toThrow('Invalid response');
  });
});

Use QA environment for integration tests

// config/test.js
module.exports = {
  apiUrl: process.env.QA_API_URL || 'https://qa-api-v3.sprinklr.com',
  accessToken: process.env.QA_ACCESS_TOKEN,
  apiKey: process.env.QA_API_KEY
};

// tests/integration.test.js
describe('Integration Tests (QA Environment)', () => {
  const client = new SprinklrClient(
    process.env.QA_ACCESS_TOKEN,
    process.env.QA_API_KEY
  );

  test('can fetch accounts from QA', async () => {
    const accounts = await client.getAccounts();
    expect(accounts).toBeDefined();
    expect(Array.isArray(accounts.data)).toBe(true);
  });

  test('pagination works correctly', async () => {
    const page1 = await client.searchCases({ page: 0, size: 10 });
    const page2 = await client.searchCases({ page: 1, size: 10 });

    expect(page1.pagination.page).toBe(0);
    expect(page2.pagination.page).toBe(1);
    expect(page1.data).not.toEqual(page2.data);
  });
});

Performance: Make it fast

Slow integrations frustrate users and waste resources.

Use appropriate timeouts

class SprinklrClient {
  constructor(accessToken, apiKey, options = {}) {
    this.accessToken = accessToken;
    this.apiKey = apiKey;
    this.timeout = options.timeout || 30000; // 30 seconds default
  }

  async request(endpoint, options = {}) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(endpoint, {
        ...options,
        signal: controller.signal,
        headers: {
          'Authorization': `Bearer ${this.accessToken}`,
          'Key': this.apiKey,
          ...options.headers
        }
      });

      clearTimeout(timeoutId);
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      if (error.name === 'AbortError') {
        throw new Error(`Request timeout after ${this.timeout}ms`);
      }
      throw error;
    }
  }
}

Use connection pooling

const https = require('https');
const fetch = require('node-fetch');

// Create agent with connection pooling
const agent = new https.Agent({
  keepAlive: true,
  keepAliveMsecs: 30000,
  maxSockets: 50,
  maxFreeSockets: 10
});

// Use agent for all requests
async function makeRequest(url) {
  return fetch(url, {
    agent,
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Key': apiKey
    }
  });
}

Monitoring: Know what's happening

You can't fix what you can't see.

Track key metrics

class MetricsCollector {
  constructor() {
    this.metrics = {
      requests: {
        total: 0,
        successful: 0,
        failed: 0
      },
      latency: {
        total: 0,
        measurements: []
      },
      errors: new Map(),
      endpoints: new Map()
    };
  }

  recordRequest(endpoint, latency, success, error = null) {
    // Track request counts
    this.metrics.requests.total++;
    if (success) {
      this.metrics.requests.successful++;
    } else {
      this.metrics.requests.failed++;
    }

    // Track latency
    this.metrics.latency.total += latency;
    this.metrics.latency.measurements.push(latency);

    // Keep only last 1000 measurements
    if (this.metrics.latency.measurements.length > 1000) {
      this.metrics.latency.measurements.shift();
    }

    // Track errors
    if (error) {
      const errorKey = `${error.code || error.message}`;
      this.metrics.errors.set(
        errorKey,
        (this.metrics.errors.get(errorKey) || 0) + 1
      );
    }

    // Track per-endpoint metrics
    if (!this.metrics.endpoints.has(endpoint)) {
      this.metrics.endpoints.set(endpoint, {
        requests: 0,
        failures: 0,
        totalLatency: 0
      });
    }

    const endpointMetrics = this.metrics.endpoints.get(endpoint);
    endpointMetrics.requests++;
    endpointMetrics.totalLatency += latency;
    if (!success) endpointMetrics.failures++;
  }

  getReport() {
    const { requests, latency, errors, endpoints } = this.metrics;

    return {
      requests: {
        ...requests,
        errorRate: ((requests.failed / requests.total) * 100).toFixed(2) + '%'
      },
      latency: {
        avg: (latency.total / requests.total).toFixed(2) + 'ms',
        p50: this.getPercentile(latency.measurements, 0.5) + 'ms',
        p95: this.getPercentile(latency.measurements, 0.95) + 'ms',
        p99: this.getPercentile(latency.measurements, 0.99) + 'ms'
      },
      topErrors: Array.from(errors.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
        .map(([error, count]) => ({ error, count })),
      topEndpoints: Array.from(endpoints.entries())
        .map(([endpoint, metrics]) => ({
          endpoint,
          requests: metrics.requests,
          avgLatency: (metrics.totalLatency / metrics.requests).toFixed(2) + 'ms',
          errorRate: ((metrics.failures / metrics.requests) * 100).toFixed(2) + '%'
        }))
        .sort((a, b) => b.requests - a.requests)
        .slice(0, 5)
    };
  }

  getPercentile(arr, percentile) {
    if (arr.length === 0) return 0;
    const sorted = arr.slice().sort((a, b) => a - b);
    const index = Math.floor(sorted.length * percentile);
    return sorted[index];
  }
}

// Usage
const metrics = new MetricsCollector();

async function makeAPICall(endpoint) {
  const startTime = Date.now();
  let success = false;
  let error = null;

  try {
    const response = await fetch(endpoint, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Key': apiKey
      }
    });

    success = response.ok;
    if (!success) {
      error = await response.json();
    }

    return response.json();
  } catch (err) {
    error = err;
    throw err;
  } finally {
    const latency = Date.now() - startTime;
    metrics.recordRequest(endpoint, latency, success, error);
  }
}

// Generate hourly reports
setInterval(() => {
  console.log('=== API Metrics Report ===');
  console.log(JSON.stringify(metrics.getReport(), null, 2));
}, 3600000); // Every hour

Production checklist

Before deploying to production, verify these items:

Security ✓

  • Credentials stored in secrets manager (not in code)
  • Different credentials for each environment
  • HTTPS enforced for all requests
  • API keys not logged or exposed in error messages
  • Credential rotation schedule implemented

Reliability ✓

  • Exponential backoff retry logic implemented
  • Circuit breakers for cascading failures
  • Timeouts configured (30s for API calls)
  • Error handling covers all scenarios
  • Graceful degradation when API is unavailable

Performance ✓

  • Batch operations used wherever possible
  • Caching implemented with appropriate TTLs
  • Connection pooling enabled
  • Request queuing to prevent overwhelming API
  • Pagination implemented for large datasets

Monitoring ✓

  • Request/error metrics tracked
  • Rate limit usage monitored
  • Alerts configured for anomalies
  • Logs centralized and searchable
  • Performance metrics tracked (p50, p95, p99 latency)

Testing ✓

  • Unit tests cover happy paths
  • Integration tests cover error scenarios
  • Load testing performed
  • QA environment tested thoroughly
  • Failure scenarios tested (network issues, timeouts, rate limits)

Next steps

Need help?

If you're building a production integration:

  1. Review this checklist - Ensure you've implemented all critical patterns
  2. Test in QA first - Never deploy untested code to production
  3. Monitor from day one - Set up metrics and alerts before launch
  4. Start small - Begin with conservative rate limits and scale up

Support resources:

  • Technical support: support.sprinklr.com
  • API documentation: This guide and related docs
  • Community forums: Share patterns with other developers