Following these best practices will help you build efficient, reliable, and maintainable integrations with the FirstQuadrant API.

Authentication & security

Secure credential storage

Never hardcode API keys or tokens in your source code:

// ❌ Bad: Hardcoded credentials
const apiKey = "fqa_abc123def456";

// ✅ Good: Environment variables
const apiKey = process.env.FIRSTQUADRANT_API_KEY;

// ✅ Good: Secure key management service
const apiKey = await keyVault.getSecret("firstquadrant-api-key");

API key rotation

Implement a rotation strategy for API keys:

  1. Generate new API keys periodically
  2. Update your applications with the new key
  3. Revoke old keys after confirming the new key works
  4. Monitor for unauthorized usage

Minimize scope

Request only the permissions your integration needs:

// ❌ Bad: Requesting all permissions
const scopes = ["urn:firstquadrant:*:*:write"];

// ✅ Good: Specific permissions only
const scopes = ["urn:firstquadrant:contact:*:read", "urn:firstquadrant:campaign:*:write"];

Request optimization

Use field selection

Only request the fields you need to reduce payload size and improve performance:

// ❌ Bad: Fetching all fields when you only need a few
const response = await fetch("/v5/contacts?limit=100");

// ✅ Good: Select specific fields
const response = await fetch("/v5/contacts?select[]=id&select[]=email&select[]=firstName&limit=100");

Batch operations

When possible, group operations to reduce API calls:

// ❌ Bad: Individual requests for each contact
for (const email of emails) {
  await createContact({ email });
}

// ✅ Good: Process in batches
async function processBatch(contacts) {
  // Use bulk endpoints when available
  // Or process with controlled concurrency
  const batchSize = 50;
  for (let i = 0; i < contacts.length; i += batchSize) {
    const batch = contacts.slice(i, i + batchSize);
    await Promise.all(batch.map((contact) => createContact(contact)));
  }
}

Implement caching

Cache frequently accessed, rarely changing data:

class CachedAPI {
  constructor(ttl = 300000) {
    // 5 minutes
    this.cache = new Map();
    this.ttl = ttl;
  }

  async getOrganization(id) {
    const cacheKey = `org:${id}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() < cached.expiry) {
      return cached.data;
    }

    const data = await fetchOrganization(id);
    this.cache.set(cacheKey, {
      data,
      expiry: Date.now() + this.ttl,
    });

    return data;
  }
}

Error handling

Implement comprehensive error handling

Handle all possible error scenarios:

async function apiRequest(endpoint, options = {}) {
  try {
    const response = await fetch(`${API_BASE}${endpoint}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
        ...options.headers,
      },
    });

    // Handle different error types
    if (!response.ok) {
      const error = await response.json();

      switch (response.status) {
        case 400:
          throw new ValidationError(error);
        case 401:
          await refreshToken();
          return apiRequest(endpoint, options); // Retry
        case 403:
          throw new PermissionError(error);
        case 404:
          throw new NotFoundError(error);
        case 429:
          await handleRateLimit(response);
          return apiRequest(endpoint, options); // Retry
        case 500:
        case 502:
        case 503:
        case 504:
          throw new ServerError(error);
        default:
          throw new APIError(error);
      }
    }

    return response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      throw new TimeoutError("Request timeout");
    }
    if (error.name === "TypeError") {
      throw new NetworkError("Network error");
    }
    throw error;
  }
}

Log errors with context

Include request details for debugging:

function logError(error, context) {
  console.error({
    timestamp: new Date().toISOString(),
    error: {
      message: error.message,
      code: error.code,
      status: error.status,
    },
    request: {
      method: context.method,
      endpoint: context.endpoint,
      requestId: context.headers?.["X-Request-Id"],
    },
    user: context.userId,
    organization: context.organizationId,
  });
}

Performance

Implement request timeouts

Prevent hanging requests:

async function fetchWithTimeout(url, options = {}, timeout = 30000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

Use pagination efficiently

Process large datasets without overwhelming your system:

async function* getAllContacts(filters = {}) {
  let cursor = null;
  const pageSize = 100; // Maximum allowed

  while (true) {
    const params = new URLSearchParams({
      ...filters,
      limit: pageSize,
      ...(cursor && { startingAfter: cursor }),
    });

    const contacts = await fetch(`/v5/contacts?${params}`);

    if (contacts.length === 0) break;

    yield contacts;

    if (contacts.length < pageSize) break;
    cursor = contacts[contacts.length - 1].id;
  }
}

// Process in streams
for await (const batch of getAllContacts()) {
  await processBatch(batch);
}

Implement connection pooling

Reuse connections for better performance:

import { Agent } from "https";

const httpsAgent = new Agent({
  keepAlive: true,
  keepAliveMsecs: 60000,
  maxSockets: 10,
});

const response = await fetch(url, {
  agent: httpsAgent,
  // ... other options
});

ID management

Use type-prefixed IDs

Always validate ID formats:

const ID_PATTERNS = {
  user: /^usr_[a-zA-Z0-9]+$/,
  organization: /^org_[a-zA-Z0-9]+$/,
  contact: /^con_[a-zA-Z0-9]+$/,
  campaign: /^cam_[a-zA-Z0-9]+$/,
  deal: /^del_[a-zA-Z0-9]+$/,
};

function validateId(id, type) {
  const pattern = ID_PATTERNS[type];
  if (!pattern || !pattern.test(id)) {
    throw new Error(`Invalid ${type} ID format: ${id}`);
  }
  return id;
}

// Usage
const contactId = validateId(inputId, "contact");

Data consistency

Handle concurrent updates

Implement optimistic locking when needed:

async function updateContact(id, updates) {
  // Fetch current version
  const current = await getContact(id);

  try {
    const updated = await apiRequest(`/contacts/${id}`, {
      method: "PATCH",
      body: JSON.stringify({
        ...updates,
        expectedVersion: current.version, // If API supports versioning
      }),
    });
    return updated;
  } catch (error) {
    if (error.code === "conflict") {
      // Handle concurrent modification
      console.warn("Contact was modified by another process");
      // Retry with fresh data or merge changes
    }
    throw error;
  }
}

Validate data before sending

Validate on the client side to avoid unnecessary API calls:

import { z } from "zod";

const ContactSchema = z.object({
  email: z.string().email(),
  firstName: z.string().min(1).max(100),
  lastName: z.string().min(1).max(100),
  phone: z
    .string()
    .regex(/^\+[1-9]\d{1,14}$/)
    .optional(),
  customProperties: z.record(z.any()).optional(),
});

function createContact(data) {
  // Validate before API call
  const validated = ContactSchema.parse(data);

  return apiRequest("/contacts", {
    method: "POST",
    body: JSON.stringify(validated),
  });
}

Monitoring & observability

Track API usage

Monitor your integration’s performance:

class APIMetrics {
  constructor() {
    this.metrics = {
      requests: 0,
      errors: 0,
      latency: [],
    };
  }

  async track(fn) {
    const start = Date.now();
    this.metrics.requests++;

    try {
      const result = await fn();
      this.metrics.latency.push(Date.now() - start);
      return result;
    } catch (error) {
      this.metrics.errors++;
      throw error;
    }
  }

  getStats() {
    const latency = this.metrics.latency;
    return {
      totalRequests: this.metrics.requests,
      errorRate: this.metrics.errors / this.metrics.requests,
      avgLatency: latency.reduce((a, b) => a + b, 0) / latency.length,
      p95Latency: latency.sort()[Math.floor(latency.length * 0.95)],
    };
  }
}

Include correlation IDs

Track requests across systems:

import { randomUUID } from "crypto";

function createRequestHeaders(correlationId = randomUUID()) {
  return {
    Authorization: `Bearer ${API_KEY}`,
    "FirstQuadrant-Organization-ID": ORGANIZATION_ID,
    "X-Correlation-ID": correlationId,
    "User-Agent": "MyApp/1.0.0",
  };
}

Integration patterns

Implement idempotency

Make operations safe to retry:

async function createCampaignIdempotent(campaign, idempotencyKey) {
  // Check if already processed
  const existing = await cache.get(`idempotent:${idempotencyKey}`);
  if (existing) return existing;

  const result = await apiRequest("/campaigns", {
    method: "POST",
    headers: {
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(campaign),
  });

  // Cache result
  await cache.set(`idempotent:${idempotencyKey}`, result, 86400000); // 24h

  return result;
}

Handle webhooks securely

If implementing webhook endpoints:

function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac("sha256", secret);
  const digest = hmac.update(payload).digest("hex");

  // Use timing-safe comparison
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}

app.post("/webhook", (req, res) => {
  const signature = req.headers["x-webhook-signature"];

  if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  // Process webhook
  processWebhook(req.body);

  // Always respond quickly
  res.status(200).send("OK");
});

Testing

Mock API responses

Test without hitting the real API:

class MockAPI {
  constructor() {
    this.responses = new Map();
  }

  setResponse(method, path, response) {
    this.responses.set(`${method}:${path}`, response);
  }

  async fetch(path, options = {}) {
    const method = options.method || "GET";
    const key = `${method}:${path}`;

    const response = this.responses.get(key);
    if (!response) {
      throw new Error(`No mock response for ${key}`);
    }

    return {
      ok: response.status >= 200 && response.status < 300,
      status: response.status,
      json: async () => response.body,
    };
  }
}

// In tests
const mockAPI = new MockAPI();
mockAPI.setResponse("GET", "/v5/contacts/con_123", {
  status: 200,
  body: { id: "con_123", email: "[email protected]" },
});

Documentation

Document your integration

Maintain clear documentation:

/**
 * FirstQuadrant API Client
 *
 * @example
 * const client = new FirstQuadrantClient({
 *   apiKey: process.env.FQ_API_KEY,
 *   organizationId: process.env.FQ_ORG_ID
 * });
 *
 * const contacts = await client.contacts.list({
 *   filter: { tags: { has: 'customer' } },
 *   limit: 50
 * });
 */
class FirstQuadrantClient {
  // Implementation
}

Summary

Key takeaways for building robust FirstQuadrant API integrations:

  1. Security First: Protect credentials, validate inputs, use minimum required permissions
  2. Handle Errors Gracefully: Implement comprehensive error handling and retry logic
  3. Optimize Performance: Use field selection, pagination, and caching
  4. Monitor Everything: Track metrics, log errors with context, use correlation IDs
  5. Test Thoroughly: Mock API responses, test error scenarios, validate edge cases
  6. Document Well: Maintain clear documentation for your integration

Following these practices will help ensure your integration is reliable, performant, and maintainable.