Skip to main content

CRM Integration Type Definitions

Source files:

  • lib/types/twenty-crm-config.types.ts - Configuration and connection types
  • lib/types/twenty-crm-entities.types.ts - Person and Company entity types
  • lib/types/twenty-crm-errors.types.ts - Error codes, structured errors, and type guards
  • lib/types/twenty-crm-sync.types.ts - Upsert operations, cache entries, and sync options

The template integrates with Twenty CRM, an open-source CRM platform. These types define the complete interface for configuration, entity mapping, error handling, and synchronization operations.


Configuration Types (twenty-crm-config.types.ts)

TwentyCrmSyncMode

Sync mode options for the CRM integration:

type TwentyCrmSyncMode = 'disabled' | 'platform' | 'direct_crm';
ModeDescription
disabledCRM sync is turned off
platformSync through the platform's event system
direct_crmDirect API calls to Twenty CRM

TwentyCrmConfig

Full CRM configuration stored in the database:

interface TwentyCrmConfig {
id: string;
baseUrl: string;
apiKey: string;
enabled: boolean;
syncMode: TwentyCrmSyncMode;
createdBy: string | null;
updatedBy: string | null;
createdAt: Date;
updatedAt: Date;
}

TwentyCrmConfigResponse

Configuration response with a masked API key for safe transmission to the admin UI:

interface TwentyCrmConfigResponse {
id: string;
baseUrl: string;
apiKey: string; // Masked value (e.g., "****key123")
enabled: boolean;
syncMode: TwentyCrmSyncMode;
updatedBy: string | null;
updatedAt: Date;
}

UpdateTwentyCrmConfigRequest

Request payload for creating or updating the CRM configuration:

interface UpdateTwentyCrmConfigRequest {
baseUrl: string;
apiKey: string;
enabled: boolean;
syncMode: TwentyCrmSyncMode;
}

TwentyCrmEnvConfig

Configuration from environment variables (used as defaults):

interface TwentyCrmEnvConfig {
baseUrl?: string;
apiKey?: string;
enabled: boolean;
syncMode: TwentyCrmSyncMode;
}

TwentyCrmTestConnectionResult

Result from testing the CRM connection:

interface TwentyCrmTestConnectionResult {
ok: boolean;
latencyMs: number;
message: string;
details?: {
status?: number;
error?: string;
};
}

TwentyCrmClientConfig

Configuration for the REST client that communicates with Twenty CRM:

interface TwentyCrmClientConfig {
baseUrl: string;
apiKey: string;
timeout?: number;
maxRetries?: number;
initialBackoffMs?: number;
maxBackoffMs?: number;
}

Generic Response Types

interface TwentyCrmSuccessResponse<T> {
success: true;
data: T;
}

interface TwentyCrmErrorResponse {
success: false;
error: TwentyCrmError;
}

type TwentyCrmResponse<T> =
TwentyCrmSuccessResponse<T> | TwentyCrmErrorResponse;

Entity Types (twenty-crm-entities.types.ts)

TwentyPerson

Represents a contact/person in Twenty CRM:

interface TwentyPerson {
external_id: string; // Maps to local clientProfile.id
name: string;
email: string;
phone?: string | null;
job_title?: string | null;
company_name?: string | null;
website?: string | null;
city?: string | null;
account_type?: string | null; // individual, business, enterprise
plan?: string | null; // free, standard, premium
total_submissions?: number | null;
}

TwentyCompany

Represents a company/organization in Twenty CRM:

interface TwentyCompany {
external_id: string; // Maps to local company.id
name: string;
domain_name?: string | null; // e.g., example.com
website?: string | null;
status?: string | null; // active, inactive
}

Error Types (twenty-crm-errors.types.ts)

TwentyCrmErrorCode

Error codes enum for categorizing CRM API errors:

enum TwentyCrmErrorCode {
TIMEOUT = 'TIMEOUT',
RATE_LIMIT = 'RATE_LIMIT',
SERVER_ERROR = 'SERVER_ERROR',
AUTH_ERROR = 'AUTH_ERROR',
NETWORK_ERROR = 'NETWORK_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
NOT_FOUND = 'NOT_FOUND',
OPERATION_FAILED = 'OPERATION_FAILED',
UNKNOWN = 'UNKNOWN',
}

TwentyCrmError

Structured error for CRM API interactions:

interface TwentyCrmError {
code: TwentyCrmErrorCode;
message: string;
status?: number;
isRetryable: boolean;
details?: {
originalError?: string;
attempt?: number;
maxRetries?: number;
[key: string]: unknown;
};
}
FieldTypeDescription
codeTwentyCrmErrorCodeMachine-readable error classification
messagestringHuman-readable error description
statusnumber?HTTP status code (if applicable)
isRetryablebooleanWhether the operation can be retried
detailsobject?Additional context including retry info

Error Functions

isTwentyCrmError

Type guard to check if an unknown value is a TwentyCrmError:

function isTwentyCrmError(error: unknown): error is TwentyCrmError;

createTwentyCrmError

Factory function to create a TwentyCrmError:

function createTwentyCrmError(
code: TwentyCrmErrorCode,
message: string,
options?: {
status?: number;
isRetryable?: boolean;
details?: TwentyCrmError['details'];
}
): TwentyCrmError;

Sync Types (twenty-crm-sync.types.ts)

UpsertResult<T>

Generic result of an upsert (create or update) operation:

interface UpsertResult<T> {
id: string; // CRM UUID assigned by Twenty CRM
externalId: string; // External ID from our system
created: boolean; // True if a new record was created
updated: boolean; // True for both create and update operations
data: T; // Full entity data from the CRM
}

UpsertCompanyInput

Input for upserting a company record:

interface UpsertCompanyInput {
external_id: string;
name: string;
domain_name?: string | null;
website?: string | null;
status?: string | null;
}

UpsertPersonInput

Input for upserting a person/contact record:

interface UpsertPersonInput {
external_id: string;
name: string;
email: string;
phone?: string | null;
job_title?: string | null;
company_name?: string | null;
website?: string | null;
city?: string | null;
account_type?: string | null;
plan?: string | null;
total_submissions?: number | null;
company_external_id?: string | null; // Links person to company
}

Cache Types

CacheEntry

Base cache entry for mapping external IDs to CRM IDs:

interface CacheEntry {
crmId: string; // CRM UUID from Twenty CRM
externalId: string; // External ID from our system
cachedAt: number; // Timestamp (ms since epoch)
}

PersonCacheEntry

Cache entry for Person entities:

interface PersonCacheEntry extends CacheEntry {
type: 'person';
}

CompanyCacheEntry

Cache entry for Company entities:

interface CompanyCacheEntry extends CacheEntry {
type: 'company';
}

AnyCacheEntry

Union of all cache entry types:

type AnyCacheEntry = PersonCacheEntry | CompanyCacheEntry;

UpsertOptions

Options for controlling upsert behavior:

interface UpsertOptions {
useCache?: boolean; // Use cache for lookups (default: true)
maxConflictRetries?: number; // Max retries for 409 conflicts (default: 3)
conflictRetryDelay?: number; // Delay between retries in ms (default: 100)
}

Usage Examples

Testing CRM connection

import type { TwentyCrmTestConnectionResult } from '@/lib/types/twenty-crm-config.types';

async function testConnection(): Promise<TwentyCrmTestConnectionResult> {
const res = await fetch('/api/admin/crm/test-connection', {
method: 'POST',
});
return res.json();
}

const result = await testConnection();
if (result.ok) {
console.log(`Connected in ${result.latencyMs}ms`);
} else {
console.error(`Connection failed: ${result.message}`);
}

Upserting a person

import type { UpsertPersonInput, UpsertOptions } from '@/lib/types/twenty-crm-sync.types';

const personInput: UpsertPersonInput = {
external_id: 'client-profile-uuid',
name: 'Jane Smith',
email: 'jane@example.com',
job_title: 'Product Manager',
company_name: 'Acme Corp',
account_type: 'business',
plan: 'premium',
total_submissions: 5,
company_external_id: 'company-uuid',
};

const options: UpsertOptions = {
useCache: true,
maxConflictRetries: 3,
conflictRetryDelay: 100,
};

Handling CRM errors

import {
TwentyCrmErrorCode,
isTwentyCrmError,
createTwentyCrmError,
} from '@/lib/types/twenty-crm-errors.types';
import type { TwentyCrmError } from '@/lib/types/twenty-crm-errors.types';

function handleCrmError(error: unknown) {
if (isTwentyCrmError(error)) {
switch (error.code) {
case TwentyCrmErrorCode.AUTH_ERROR:
console.error('Authentication failed - check API key');
break;
case TwentyCrmErrorCode.RATE_LIMIT:
console.warn('Rate limited - will retry');
break;
case TwentyCrmErrorCode.TIMEOUT:
if (error.isRetryable) {
console.warn('Timeout - retrying...');
}
break;
default:
console.error(`CRM error: ${error.message}`);
}
}
}

Using the generic response type

import type {
TwentyCrmResponse,
} from '@/lib/types/twenty-crm-config.types';
import type { TwentyPerson } from '@/lib/types/twenty-crm-entities.types';

function handlePersonResponse(
response: TwentyCrmResponse<TwentyPerson>
) {
if (response.success) {
console.log('Person synced:', response.data.name);
} else {
console.error(
`Sync failed [${response.error.code}]: ${response.error.message}`
);
if (response.error.isRetryable) {
// Schedule retry
}
}
}

Configuring the CRM client

import type { TwentyCrmClientConfig } from '@/lib/types/twenty-crm-config.types';

const clientConfig: TwentyCrmClientConfig = {
baseUrl: 'https://crm.example.com',
apiKey: 'your-api-key',
timeout: 10000,
maxRetries: 3,
initialBackoffMs: 200,
maxBackoffMs: 5000,
};

Design Notes

External ID Mapping

The CRM integration uses external_id fields to maintain bidirectional mapping between local entities and CRM records. This enables:

  • Idempotent upserts - The same external ID always maps to the same CRM record
  • Cache optimization - External-to-CRM ID mappings are cached to reduce API calls
  • Conflict resolution - 409 conflicts during parallel upserts are automatically retried

Retry Strategy

The client uses exponential backoff with the following defaults:

  • Initial backoff: 200ms
  • Max backoff: 5000ms
  • Max retries: 3
  • The isRetryable flag on errors indicates whether automatic retry is appropriate

Error Classification

Errors are classified by TwentyCrmErrorCode to enable appropriate handling:

  • AUTH_ERROR - Invalid or expired API key
  • RATE_LIMIT - Too many requests (retryable)
  • TIMEOUT - Request timed out (retryable)
  • VALIDATION_ERROR - Invalid data sent to CRM
  • NOT_FOUND - Entity does not exist
  • NETWORK_ERROR - Connection issues (retryable)