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
.envfile intoprocess.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:
-
Controller Layer (
src/controllers/): Thensps.controller.tsfile is the entry point for incoming NSPS events. It validates the event structure and routes the event to the appropriate handler in theZohoService. -
Service Layer (
src/services/): Thezoho.service.tsfile encapsulates all the logic for interacting with the Zoho CRM API. It provides methods for creating, updating, and finding contacts and SIM card records. Thensps.service.tsfile provides helper functions for creating standardized success and error responses. Thelogger.service.tsfile configures and provides the application's logger. -
Model Layer (
src/models/): Thensps.model.tsfile defines the data structures and validation schemas for the NSPS events usingzod. -
Utility Layer (
src/utils/): Themapper.tsfile is responsible for transforming data from the NSPS event format to the Zoho CRM record format. Thevalidator.tsfile 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
-
Create Zoho Developer Console Application
- Go to Zoho Developer Console
- Create a new Server-based Application
- Note down Client ID and Client Secret
-
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:
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.
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.
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:
-
Schema Validation: The overall structure of the event is validated against a
zodschema invalidateNSPSEvent. -
Required Field Validation: The
validateRequiredFieldsfunction checks for the presence of fields that are conditionally required based on theevent_type.src/utils/validator.tsexport 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.
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:
This ensures the SDK correctly maps the selected option to the Zoho CRM picklist field.
Related Records (Lookups)
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.
// ...
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.
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.
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_PBidentifier.- 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_Cardidentifier.- 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_PBidentifier.- 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 itsRelated_Contactfield tonull.
handleSimReplaced (SIM/Replaced)
- The system successfully finds or creates a Contact by the
I_PBidentifier.- 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_Contactcleared). - If not found — a new SIM_Card record is created without linking to any contact.
- If found — it is unlinked from the 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.
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 tokenVALIDATION_ERROR(422) - Invalid request data or missing required fieldsSERVICE_ERROR(500) - Internal server errorCONNECTION_ERROR(503) - External service (Zoho CRM) unavailableRATE_LIMIT_ERROR(429) - Too many requestsINTERNAL_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 levelslogs/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:
- Initializes the Zoho CRM SDK: It calls
initializeZohoSDK()to ensure the SDK is configured and ready before any API calls are made. - Creates the Express Application: It calls the
createApp()function to set up the Express server, including all middleware and routes. - Starts the Server: It listens on the configured port and logs a confirmation message upon successful startup.
- Handles Graceful Shutdown: It sets up listeners for
SIGTERMandSIGINTsignals to ensure the server shuts down gracefully, closing the server and exiting the process cleanly.
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
helmetto set various security-related HTTP headers andcorsto enable Cross-Origin Resource Sharing. - Request Logging: It uses
morganto log incoming HTTP requests. - Body Parsing: It configures
express.json()andexpress.urlencoded()to parse incoming request bodies. - Health Check Endpoint: It defines a
/healthendpoint that can be used to monitor the application's status. - Event Processing Route: It sets up the main
/process-eventroute, protected by theverifyBearerTokenauthentication middleware. - Error Handling: It includes middleware for handling 404 Not Found errors and a global error handler to catch any unhandled exceptions.
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;
}