Skip to content

Zoho Crm Connector

A production-ready TypeScript/Express connector that processes NSPS provisioning events and synchronizes data to Zoho CRM. The connector handles four event types (SIM/Updated, SIM/Created, SIM/Deleted, SIM/Replaced) with proper authentication, validation, error handling, and logging.

You can find the full example repository here: Zoho CRM Connector

Project Structure

The project is organized into the following main directories:

  • src/: Contains the main source code for the application.
    • config/: Handles application configuration, including environment variables and Zoho CRM SDK initialization.
    • controllers/: Manages incoming HTTP requests and routes them to the appropriate services.
    • middlewares/: Provides middleware for authentication, error handling, and other cross-cutting concerns.
    • models/: Defines the data structures and validation schemas for the application.
    • services/: Contains the core business logic, including the Zoho CRM integration.
    • utils/: Provides utility functions for data mapping, validation, and other tasks.
  • logs/: Stores log files generated by the application.
  • resources/: Contains resources for the Zoho CRM SDK.

Dependencies

The project relies on several key dependencies to function correctly:

  • @zohocrm/typescript-sdk-8.0: The official TypeScript SDK for Zoho CRM, used for all interactions with the Zoho API.
  • express: A minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
  • dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.
  • zod: A TypeScript-first schema declaration and validation library. It's used to validate the structure of incoming NSPS events.
  • winston: A versatile logging library for Node.js, used for application-wide logging.
  • cors: A Node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.
  • helmet: Helps secure Express apps by setting various HTTP headers.
  • morgan: An HTTP request logger middleware for Node.js.

Architecture Details

The application follows a layered architecture:

  1. Controller Layer (src/controllers/): The nsps.controller.ts file is the entry point for incoming NSPS events. It validates the event structure and routes the event to the appropriate handler in the ZohoService.

  2. Service Layer (src/services/): The zoho.service.ts file encapsulates all the logic for interacting with the Zoho CRM API. It provides methods for creating, updating, and finding contacts and SIM card records. The nsps.service.ts file provides helper functions for creating standardized success and error responses. The logger.service.ts file configures and provides the application's logger.

  3. Model Layer (src/models/): The nsps.model.ts file defines the data structures and validation schemas for the NSPS events using zod.

  4. Utility Layer (src/utils/): The mapper.ts file is responsible for transforming data from the NSPS event format to the Zoho CRM record format. The validator.ts file provides functions for validating the structure and required fields of NSPS events.

Zoho CRM Setup

Required Custom Fields

The connector requires the following custom fields in your Zoho CRM:

Contacts Module

Field Name Type Description
Bill_Status_PB Pick List The billing status of the account
Billing_Model_PB Pick List The account type
Blocked_PB Boolean Indicates whether account's calls
Product_Name_PB Text The name of the account's product
Account_Status_PB Pick List The current status of the account
I_Account_PB Number Internal ID of the account
I_PB Text Unique identifier for PortaBilling accounts

SIM_Cards Module

Note: The SIM_Cards module is not available by default in Zoho CRM. It needs to be created manually as a custom module.

Field Name Type Description
I_SIM_Card Number Unique identifier for SIM cards
IMSI Text International Mobile Subscriber Identity
MSISDN Text Mobile Station International Subscriber Directory Number
Status Pick List SIM card status
Related_Contact Lookup Links SIM card to contact

OAuth 2.0 Setup

  1. Create Zoho Developer Console Application

  2. Generate Refresh Token

    • Use the authorization URL to get authorization code
    • Exchange authorization code for refresh token
    • Store refresh token securely

Environment Configuration

Environment variables are managed using the dotenv package and validated using zod. The configuration is defined in src/config/index.ts.

Key environment variables include:

src/config/index.ts
import { z } from 'zod';

const envSchema = z.object({
    APP_NAME: z.string().default('zoho-crm-connector'),
    NODE_ENV: z.enum(['development', 'production']).default('development'),
    PORT: z.coerce.number().min(1).max(65535).default(3000),

    LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
    DEBUG: z.coerce.boolean().default(false),

    // NSPS Authentication Bearer token
    API_TOKEN: z.string().min(1, 'API_TOKEN is required'),

    ZOHO_CRM_ACCOUNTS_URL: z.string().url().default('https://www.zohoapis.eu'),
    ZOHO_CRM_API_DOMAIN: z.string().url().default('https://accounts.zoho.eu'),

    // Zoho CRM Authentication
    ZOHO_CRM_CLIENT_ID: z.string().min(1, 'ZOHO_CRM_CLIENT_ID is required'),
    ZOHO_CRM_CLIENT_SECRET: z
        .string()
        .min(1, 'ZOHO_CRM_CLIENT_SECRET is required'),
    ZOHO_CRM_REFRESH_TOKEN: z
        .string()
        .min(1, 'ZOHO_CRM_REFRESH_TOKEN is required'),
});

const parseResult = envSchema.safeParse(process.env);

if (!parseResult.success) {
    console.error('Environment validation failed:');
    console.error(parseResult.error.format());
    process.exit(1);
}

export const config = parseResult.data;

Zoho CRM SDK Configuration

The Zoho CRM SDK is initialized in the initializeZohoSDK function in src/config/index.ts. This asynchronous function configures and initializes the Zoho CRM SDK with proper logging, environment, authentication, and token storage.

src/config/index.ts
import * as ZOHOCRMSDK from '@zohocrm/typescript-sdk-8.0';
// ...

// Zoho CRM SDK initialization
let zohoSDKInitialized = false;

export async function initializeZohoSDK(): Promise<void> {
    if (zohoSDKInitialized) {
        return;
    }

    try {
        // Configure logger
        const logger: ZOHOCRMSDK.Logger = new ZOHOCRMSDK.LogBuilder()
            .level(ZOHOCRMSDK.Levels.INFO)
            .filePath(path.join(process.cwd(), 'logs', 'zoho-sdk.log'))
            .build();

        // Set environment to EU data center
        const environment: ZOHOCRMSDK.Environment =
            ZOHOCRMSDK.EUDataCenter.PRODUCTION();

        // Configure OAuth token
        const token: ZOHOCRMSDK.OAuthToken = new ZOHOCRMSDK.OAuthBuilder()
            .clientId(config.ZOHO_CRM_CLIENT_ID)
            .clientSecret(config.ZOHO_CRM_CLIENT_SECRET)
            .refreshToken(config.ZOHO_CRM_REFRESH_TOKEN)
            .redirectURL('https://www.zoho.com')
            .build();

        // Configure token storage (FileStore)
        const tokenStore: ZOHOCRMSDK.FileStore = new ZOHOCRMSDK.FileStore(
            path.join(process.cwd(), 'zohooauth_tokens.txt')
        );

        // Configure SDK options
        const sdkConfig: ZOHOCRMSDK.SDKConfig =
            new ZOHOCRMSDK.SDKConfigBuilder()
                .pickListValidation(false)
                .autoRefreshFields(true)
                .build();

        // Set resource path
        const resourcePath: string = path.join(process.cwd(), 'resources');

        // Initialize SDK
        await (await new ZOHOCRMSDK.InitializeBuilder())
            .environment(environment)
            .token(token)
            .store(tokenStore)
            .SDKConfig(sdkConfig)
            .resourcePath(resourcePath)
            .logger(logger)
            .initialize();

        zohoSDKInitialized = true;
        console.log('Zoho CRM SDK initialized successfully');
    } catch (error) {
        console.error('Failed to initialize Zoho CRM SDK:', error);
        throw error;
    }
}

Data Models

The nsps.model.ts file is central to the connector's data handling. It defines the TypeScript interfaces for the NSPS event structure and uses zod to create validation schemas that ensure data integrity.

src/models/nsps.model.ts
import { z } from 'zod';

export const ALLOWED_EVENT_TYPES = [
    'SIM/Updated',
    'SIM/Created',
    'SIM/Deleted',
    'SIM/Replaced',
] as const;

// Enums for NSPS Event Types
export const EventTypeSchema = z.enum(ALLOWED_EVENT_TYPES);

// Enums for AccountInfo
export const BillStatusSchema = z.enum([
    'active',
    'suspended',
    'inactive',
    'terminated',
]);
export const BillingModelSchema = z.enum([
    'debit_account',
    'recharge_voucher',
    'credit_account',
    'alias',
    'beneficiary',
]);
export const AccountStatusSchema = z.enum([
    'active',
    'customer_exported',
    'expired',
    'quarantine',
    'screening',
    'closed',
    'inactive',
    'customer_suspended',
    'customer_limited',
    'customer_provisionally_terminated',
    'blocked',
    'customer_blocked',
    'not_yet_active',
    'credit_exceeded',
    'overdraft',
    'customer_has_no_available_funds',
    'customer_credit_exceed',
    'zero_balance',
    'customer_suspension_delayed',
    'customer_limiting_delayed',
    'frozen',
]);

// Enum for SIM
export const SimStatusSchema = z.enum([
    'available',
    'reserved',
    'used',
    'disposed',
]);

// Zod validation schemas
export const SimInfoSchema = z.object({
    i_sim_card: z.number(),
    imsi: z.string(),
    msisdn: z.string(),
    status: SimStatusSchema.optional(),
});

export const AccountInfoSchema = z.object({
    firstname: z.string(),
    lastname: z.string(),
    email: z.string().email().optional(),
    phone1: z.string().optional(),
    bill_status: BillStatusSchema.optional(),
    billing_model: BillingModelSchema.optional(),
    blocked: z.boolean().optional(),
    product_name: z.string().optional(),
    status: AccountStatusSchema.optional(),
    i_account: z.number().optional(),
    id: z.string(),
});

export const PBDataSchema = z.object({
    account_info: AccountInfoSchema.optional(),
    sim_info: SimInfoSchema.optional(),
    prev_sim_info: SimInfoSchema.optional(),
});

export const EventDataSchema = z.object({
    event_type: EventTypeSchema,
    variables: z.record(z.any()).optional(),
});

export const NSPSEventSchema = z.object({
    event_id: z.string(),
    data: EventDataSchema,
    pb_data: PBDataSchema,
    handler_id: z.string(),
    created_at: z.string().optional(),
    updated_at: z.string().optional(),
    status: z.string(),
});

Event Validation

Incoming event data is rigorously validated at two levels:

  1. Schema Validation: The overall structure of the event is validated against a zod schema in validateNSPSEvent.

    src/utils/validator.ts
    export function validateNSPSEvent(data: any): ValidationResult {
        try {
            const event = NSPSEventSchema.parse(data);
            EventTypeSchema.parse(event.data.event_type);
    
            return {
                success: true,
                data: event,
            };
        } catch (error) {
            // ... returns formatted validation errors
        }
    }
    
  2. Required Field Validation: The validateRequiredFields function checks for the presence of fields that are conditionally required based on the event_type.

    src/utils/validator.ts
    export function validateRequiredFields(
        event: NSPSEvent
    ): ValidationError[] {
        const errors: ValidationError[] = [];
    
        if (!event.pb_data.account_info) {
            errors.push({
                loc: ['pb_data', 'account_info'],
                msg: 'account_info is required',
                type: 'missing_field',
            });
            return errors;
        }
    
        // ... additional checks based on event_type
    
        return errors;
    }
    

Data Mapping

For example, the mapAccountInfoToContact function in src/utils/mapper.ts maps an AccountInfo object to a Zoho CRM Record object for the Contacts module.

src/utils/mapper.ts
export function mapAccountInfoToContact(
    accountInfo: AccountInfo
): ZOHOCRMSDK.Record.Record {
    const {
        firstname,
        lastname,
        email,
        // ...
    } = accountInfo;

    const record = new ZOHOCRMSDK.Record.Record();

    if (firstname) {
        record.addKeyValue('First_Name', firstname);
    }

    if (lastname) {
        record.addKeyValue('Last_Name', lastname);
    }
    // ...
    return record;
}

Similarly, the mapSimInfoToSimCard function in src/utils/mapper.ts maps an SimInfo object to a Zoho CRM Record object for the SIM_Cards module.

When creating or updating records via the Zoho CRM SDK, it’s critical to use the correct data types and helper classes for specific field types. Failing to do so can result in validation errors or API rejections.

Pick List Fields

For picklist fields, values must be wrapped in a Choice object.

Example:

record.addKeyValue('Status', new ZOHOCRMSDK.Choice(status));

This ensures the SDK correctly maps the selected option to the Zoho CRM picklist field.

When assigning related records (for example, linking a Contact to another entity), you must create a new Record instance and set its ID.

Example:

const relatedContact = new ZOHOCRMSDK.Record.Record();
relatedContact.setId(BigInt(relatedContactId));

record.addKeyValue('Related_Contact', relatedContact);

Event Processing

The core logic of the connector resides in the ZohoService class (src/services/zoho.service.ts), which handles all interactions with the Zoho CRM API. This service is responsible for processing NSPS events and performing the corresponding CRUD (Create, Read, Update, Delete) operations in Zoho CRM.

Event Routing

The nspsController receives an event, validates it, and routes it to the appropriate handler method in ZohoService based on the event_type.

src/controllers/nsps.controller.ts
// ...
switch (eventType) {
    case 'SIM/Updated':
        await zohoService.handleSimUpdated(event);
        break;
    case 'SIM/Created':
        await zohoService.handleSimCreated(event);
        break;
    case 'SIM/Deleted':
        await zohoService.handleSimDeleted(event);
        break;
    case 'SIM/Replaced':
        await zohoService.handleSimReplaced(event);
        break;
    default:
        throw new Error(`Unsupported event type: ${eventType}`);
}
//...

Core ZohoService Methods

The ZohoService class includes several key methods for managing Contacts and SIM_Cards records in Zoho CRM.

findContactByIPB: This method searches for an existing contact using the I_PB field, which serves as a unique identifier for PortaBilling accounts.

src/services/zoho.service.ts
async findContactByIPB(i_pb: string): Promise<string | null> {
  try {
    const criteria = createContactSearchCriteria(i_pb);
    const recordOperations = new ZOHOCRMSDK.Record.RecordOperations(this.CONTACTS_MODULE);
    const request = new ZOHOCRMSDK.ParameterMap();
    request.add(ZOHOCRMSDK.Record.SearchRecordsParam.CRITERIA, criteria);

    const response = await recordOperations.searchRecords(request, new ZOHOCRMSDK.HeaderMap());

    if (response && response.getStatusCode() === 200) {
      const responseObject = response.getObject();
      if (responseObject instanceof ZOHOCRMSDK.Record.ResponseWrapper) {
        const records = responseObject.getData();
        if (records.length > 0) {
          return records[0]!.getId().toString();
        }
      }
    }
    return null;
  } catch (error) {
    // ...
  }
}

createContact and updateContact: These methods handle the creation and updating of contact records. They map the account_info from the NSPS event to the corresponding fields in the Contacts module.

src/services/zoho.service.ts
async createContact(accountInfo: AccountInfo): Promise<string> {
  try {
    const recordOperations = new ZOHOCRMSDK.Record.RecordOperations(this.CONTACTS_MODULE);
    const request = new ZOHOCRMSDK.Record.BodyWrapper();
    const record = mapAccountInfoToContact(accountInfo);
    request.setData([record]);

    const response = await recordOperations.createRecords(request, new ZOHOCRMSDK.HeaderMap());
    // ... extract and return contact ID
  } catch (error) {
    // ...
  }
}

findSimCardByISimCard, createSimCard, and updateSimCard

Similar to the contact methods, these functions manage the lifecycle of SIM_Cards records, using I_SIM_Card as the unique identifier.

handleSimUpdated (SIM/Updated), handleSimCreated (SIM/Created)

  • The system successfully finds or creates a Contact in Zoho CRM using the I_PB identifier.
    • If the contact already exists — it is updated.
    • If it does not exist — a new contact is created.
    • The contact’s id is retrieved for further linking.
  • The system then finds or creates a SIM_Card record using the I_SIM_Card identifier.
    • If the SIM card exists — it is updated and linked to the corresponding contact id.
    • If not — a new SIM card record is created and linked to the same contact.

handleSimDeleted (SIM/Deleted)

  • The system successfully finds a Contact by the I_PB identifier.
    • If it exists — the contact is updated.
    • If it does not exist — a new contact is created.
  • If a SIM_Card was previously linked to this contact (based on pb_data.sim_info.i_sim_card), the system unlinks the SIM card by setting its Related_Contact field to null.

handleSimReplaced (SIM/Replaced)

  • The system successfully finds or creates a Contact by the I_PB identifier.
    • If the contact exists — it is updated.
    • If it does not exist — a new contact is created and its id is retrieved.
  • The old SIM card (from pb_data.prev_sim_info) is correctly processed:
    • If found — it is unlinked from the contact (Related_Contact cleared).
    • If not found — a new SIM_Card record is created without linking to any contact.
  • The new SIM card (from pb_data.sim_info) is correctly processed:
    • If it exists — it is linked to the contact id.
    • If it does not exist — a new SIM_Card record is created and linked to the same contact. The service includes specific handlers for each NSPS event type:

Middleware and Authentication

The connector uses a chain of Express middleware to handle security, logging, and authentication.

NSPS Authorization

Authentication is handled by the verifyBearerToken middleware, which checks for a valid Bearer token in the Authorization header of incoming requests.

src/middlewares/auth.middleware.ts
export function verifyBearerToken(
    req: Request,
    res: Response,
    next: NextFunction
): void {
    try {
        const authHeader = req.headers.authorization;

        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            logAuthFailure('Invalid Authorization header format', req.ip);
            res.status(401).json({
                message: 'Authentication failed',
                error: 'Invalid Authorization header format',
                type: 'AUTHENTICATION_ERROR',
            });
            return;
        }

        const token = authHeader.substring(7);

        if (token !== config.API_TOKEN) {
            logAuthFailure('Invalid token', req.ip);
            res.status(401).json({
                message: 'Authentication failed',
                error: 'Invalid API token',
                type: 'AUTHENTICATION_ERROR',
            });
            return;
        }

        next();
    } catch (error) {
        // ...
    }
}

Error Handling

Errors are handled by a global error handler middleware defined in src/middlewares/error.middleware.ts. The asyncHandler utility is used to wrap asynchronous route handlers and catch any unhandled exceptions, ensuring that the server remains stable.

The connector returns NSPS-compliant error responses with the following error types:

  • AUTHENTICATION_ERROR (401) - Invalid or missing API token
  • VALIDATION_ERROR (422) - Invalid request data or missing required fields
  • SERVICE_ERROR (500) - Internal server error
  • CONNECTION_ERROR (503) - External service (Zoho CRM) unavailable
  • RATE_LIMIT_ERROR (429) - Too many requests
  • INTERNAL_ERROR (500) - Unexpected application error

Logging

Logging is implemented using the winston library. The logger is configured in src/services/logger.service.ts. The application logs HTTP requests, Zoho CRM operations, and any errors that occur.

Logs are written to:

  • logs/combined.log - All log levels
  • logs/error.log - Error level only
  • Console output (in development mode)

Log levels: error, warn, info, debug

Server Setup

The server is initialized and started in the startServer function in src/server.ts. This function serves as the main entry point of the application and performs the following key steps:

  1. Initializes the Zoho CRM SDK: It calls initializeZohoSDK() to ensure the SDK is configured and ready before any API calls are made.
  2. Creates the Express Application: It calls the createApp() function to set up the Express server, including all middleware and routes.
  3. Starts the Server: It listens on the configured port and logs a confirmation message upon successful startup.
  4. Handles Graceful Shutdown: It sets up listeners for SIGTERM and SIGINT signals to ensure the server shuts down gracefully, closing the server and exiting the process cleanly.
src/server.ts
async function startServer(): Promise<void> {
    try {
        // Initialize Zoho CRM SDK
        logger.info('Initializing Zoho CRM SDK...');
        await initializeZohoSDK();

        // Create Express app
        const app = createApp();

        // Get port from environment
        const port = process.env['PORT'] || 3000;

        // Start server
        const server = app.listen(port, () => {
            // ...
        });
        // ...
    } catch (error) {
        logger.error('Failed to start server:', error);
        process.exit(1);
    }
}

Express Application Setup

The createApp function in src/app.ts is responsible for configuring the Express application. It sets up the entire middleware chain and defines the application's routes.

  • Security Middleware: It uses helmet to set various security-related HTTP headers and cors to enable Cross-Origin Resource Sharing.
  • Request Logging: It uses morgan to log incoming HTTP requests.
  • Body Parsing: It configures express.json() and express.urlencoded() to parse incoming request bodies.
  • Health Check Endpoint: It defines a /health endpoint that can be used to monitor the application's status.
  • Event Processing Route: It sets up the main /process-event route, protected by the verifyBearerToken authentication middleware.
  • Error Handling: It includes middleware for handling 404 Not Found errors and a global error handler to catch any unhandled exceptions.
src/app.ts
export function createApp(): express.Application {
  const app = express();

  // Security middleware
  app.use(helmet());
  app.use(cors({
    origin: config.NODE_ENV === 'production' ? false : true,
    credentials: true
  }));

  // Request logging
  app.use(morgan('combined', { stream: httpLogStream }));

  // Body parsing middleware
  app.use(express.json({ limit: '10mb' }));
  app.use(express.urlencoded({ extended: true, limit: '10mb' }));

  // Health check endpoint (no auth required)
  app.get('/health', (_req: Request, res: Response) => ... );

  // NSPS event processing endpoint (requires authentication)
  app.post('/process-event', verifyBearerToken, nspsController.processEvent);

  // Handle unsupported methods on existing routes
  app.all('/process-event', methodNotAllowedHandler);

  // 404 handler for undefined routes
  app.use(notFoundHandler);

  // Global error handler (must be last)
  app.use(errorHandler);

  return app;
}