Skip to main content

Twenty CRM Utilities

The Twenty CRM integration includes two utility modules that support the REST client: a client utilities module (lib/utils/twenty-crm-client.utils.ts) for retry logic, backoff, and error sanitization, and a validation module (lib/utils/twenty-crm-validation.ts) for Zod-based configuration validation and API key masking.

Client Utilities

The client utilities module (twenty-crm-client.utils.ts) provides helpers for robust HTTP request handling with exponential backoff and retry logic.

generateIdempotencyKey

Generates a unique UUID v4 for POST/PUT request deduplication:

import { generateIdempotencyKey } from '@/lib/utils/twenty-crm-client.utils';

const key = generateIdempotencyKey();
// "550e8400-e29b-41d4-a716-446655440000"

Uses crypto.randomUUID() for RFC 4122 compliance.

calculateExponentialBackoff

Calculates a retry delay using exponential backoff with jitter to prevent the thundering herd problem:

import { calculateExponentialBackoff } from '@/lib/utils/twenty-crm-client.utils';

calculateExponentialBackoff(0); // ~1000ms (1s + jitter)
calculateExponentialBackoff(1); // ~2000ms (2s + jitter)
calculateExponentialBackoff(2); // ~4000ms (4s + jitter)
calculateExponentialBackoff(3); // ~8000ms (8s + jitter)

Implementation

export function calculateExponentialBackoff(
attempt: number,
initialBackoffMs: number = DEFAULT_INITIAL_BACKOFF_MS,
maxBackoffMs: number = DEFAULT_MAX_BACKOFF_MS
): number {
const exponentialDelay = initialBackoffMs * Math.pow(2, attempt);
const jitter = Math.random() * MAX_JITTER_MS;
return Math.min(exponentialDelay + jitter, maxBackoffMs);
}

The formula is: min(initialBackoff * 2^attempt + jitter, maxBackoff)

ParameterDefaultDescription
attemptRequiredCurrent retry attempt (0-indexed)
initialBackoffMsDEFAULT_INITIAL_BACKOFF_MSBase delay in ms
maxBackoffMsDEFAULT_MAX_BACKOFF_MSMaximum delay cap in ms

The random jitter (between 0 and MAX_JITTER_MS) prevents multiple clients from retrying at the same time after a server recovers.

shouldRetryRequest

Determines whether a failed request should be retried based on the error type, HTTP status code, and attempt count:

import { shouldRetryRequest } from '@/lib/utils/twenty-crm-client.utils';

shouldRetryRequest(429, error, 1, 3); // true (rate limited)
shouldRetryRequest(500, error, 1, 3); // true (server error)
shouldRetryRequest(401, error, 1, 3); // false (auth error)
shouldRetryRequest(200, error, 4, 3); // false (max retries exceeded)

Retry Conditions

The function returns true when any of the following conditions are met and the attempt count has not exceeded maxRetries:

ConditionExamples
Retryable HTTP status code408 (Timeout), 429 (Rate Limited), 5xx (Server Errors)
Timeout errorAbortError from fetch timeout
Network errorTypeError from fetch failures
Connection errorECONNRESET, ENOTFOUND, ETIMEDOUT, ECONNREFUSED

Implementation

export function shouldRetryRequest(
status: number | undefined,
error: Error | unknown,
attempt: number,
maxRetries: number
): boolean {
if (attempt > maxRetries) return false;

// Retryable status codes (408, 429, 5xx)
if (status && RETRYABLE_STATUS_CODES.includes(status)) return true;

// Timeout errors
if (error instanceof Error && error.name === 'AbortError') return true;

// Network errors from fetch
if (error instanceof TypeError && error.message.includes('fetch'))
return true;

// Connection reset and similar errors
if (error && typeof error === 'object' && 'code' in error) {
const networkErrorCodes = [
'ECONNRESET', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED',
];
if (networkErrorCodes.includes(error.code)) return true;
}

return false;
}

sanitizeErrorForLogging

Removes sensitive information (API keys) from error messages before logging:

import { sanitizeErrorForLogging } from '@/lib/utils/twenty-crm-client.utils';

const message = sanitizeErrorForLogging(
new Error('Auth failed with key sk_live_abc123xyz'),
'sk_live_abc123xyz'
);
// "Auth failed with key ****xyz"

The function handles Error objects, strings, and objects with a message property. All occurrences of the API key are replaced with a masked version.

delay

A simple promise-based delay utility for implementing backoff pauses between retries:

import { delay } from '@/lib/utils/twenty-crm-client.utils';

await delay(2000); // Wait 2 seconds

Validation Utilities

The validation module (twenty-crm-validation.ts) provides Zod schemas and helper functions for validating Twenty CRM configuration.

Sync Modes

The module defines the valid synchronization modes:

const SYNC_MODE_VALUES = ['disabled', 'platform', 'direct_crm'] as const;
ModeDescription
disabledCRM sync is turned off
platformSync through the platform API
direct_crmDirect connection to Twenty CRM API

Zod Schemas

Four schemas are provided for validating CRM configuration fields:

syncModeSchema

import { syncModeSchema } from '@/lib/utils/twenty-crm-validation';

syncModeSchema.parse('platform'); // OK
syncModeSchema.parse('invalid'); // Throws ZodError

baseUrlSchema

Validates that the URL is non-empty, well-formed, and uses HTTP or HTTPS:

import { baseUrlSchema } from '@/lib/utils/twenty-crm-validation';

baseUrlSchema.parse('https://crm.example.com'); // OK
baseUrlSchema.parse('ftp://crm.example.com'); // Throws (wrong protocol)
baseUrlSchema.parse(''); // Throws (empty)

apiKeySchema

Validates that the API key is non-empty and at least 10 characters:

import { apiKeySchema } from '@/lib/utils/twenty-crm-validation';

apiKeySchema.parse('sk_live_abcdef1234'); // OK
apiKeySchema.parse('short'); // Throws (under 10 chars)
apiKeySchema.parse(''); // Throws (empty)

updateTwentyCrmConfigSchema

The complete schema for configuration update requests:

import { updateTwentyCrmConfigSchema } from '@/lib/utils/twenty-crm-validation';

const result = updateTwentyCrmConfigSchema.safeParse({
baseUrl: 'https://crm.example.com',
apiKey: 'sk_live_abcdef1234',
enabled: true,
syncMode: 'platform',
});

if (result.success) {
// result.data is typed as ValidatedTwentyCrmConfigUpdate
console.log(result.data.baseUrl);
}

The schema validates these fields:

FieldTypeValidation
baseUrlstringNon-empty, valid URL, HTTP/HTTPS protocol
apiKeystringNon-empty, at least 10 characters
enabledbooleanBoolean value
syncModestringOne of 'disabled', 'platform', 'direct_crm'

maskApiKey

Masks an API key for safe display, showing only the last 4 characters:

import { maskApiKey } from '@/lib/utils/twenty-crm-validation';

maskApiKey('sk_live_abcdef1234'); // "****1234"
maskApiKey('ab'); // "****" (too short)
maskApiKey(''); // "****"

Implementation

export function maskApiKey(apiKey: string): string {
if (!apiKey || apiKey.length < 4) {
return '****';
}
const lastFourChars = apiKey.slice(-4);
return `****${lastFourChars}`;
}

validateTwentyCrmConfig

A convenience wrapper around updateTwentyCrmConfigSchema.safeParse():

import { validateTwentyCrmConfig } from '@/lib/utils/twenty-crm-validation';

const result = validateTwentyCrmConfig(requestBody);

if (!result.success) {
return Response.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
);
}

// result.data is validated and typed
await updateCrmConfig(result.data);

Usage Pattern: Retry Loop

A typical usage combining the client utilities in a retry loop:

import {
shouldRetryRequest,
calculateExponentialBackoff,
delay,
sanitizeErrorForLogging,
generateIdempotencyKey,
} from '@/lib/utils/twenty-crm-client.utils';

async function makeRequestWithRetry(
url: string,
apiKey: string,
maxRetries: number = 3
) {
const idempotencyKey = generateIdempotencyKey();

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
'Idempotency-Key': idempotencyKey,
},
});

if (!response.ok) {
if (shouldRetryRequest(response.status, null, attempt, maxRetries)) {
const backoff = calculateExponentialBackoff(attempt);
await delay(backoff);
continue;
}
throw new Error(`Request failed: ${response.status}`);
}

return await response.json();
} catch (error) {
if (shouldRetryRequest(undefined, error, attempt, maxRetries)) {
const backoff = calculateExponentialBackoff(attempt);
await delay(backoff);
continue;
}
const safeMessage = sanitizeErrorForLogging(error, apiKey);
throw new Error(safeMessage);
}
}
}

Source Files

FilePurpose
lib/utils/twenty-crm-client.utils.tsHTTP retry, backoff, and error sanitization
lib/utils/twenty-crm-validation.tsZod schemas and config validation
lib/config/twenty-crm.config.tsDefault backoff/retry constants