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:
- Generate new API keys periodically
- Update your applications with the new key
- Revoke old keys after confirming the new key works
- 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,
});
}
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);
}
}
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:
- Security First: Protect credentials, validate inputs, use minimum required permissions
- Handle Errors Gracefully: Implement comprehensive error handling and retry logic
- Optimize Performance: Use field selection, pagination, and caching
- Monitor Everything: Track metrics, log errors with context, use correlation IDs
- Test Thoroughly: Mock API responses, test error scenarios, validate edge cases
- Document Well: Maintain clear documentation for your integration
Following these practices will help ensure your integration is reliable, performant, and maintainable.