Skip to content

Instantly share code, notes, and snippets.

@malixsys
Created September 29, 2025 12:43
Show Gist options
  • Select an option

  • Save malixsys/7607e59c55e827c3d721b2e3b5512d74 to your computer and use it in GitHub Desktop.

Select an option

Save malixsys/7607e59c55e827c3d721b2e3b5512d74 to your computer and use it in GitHub Desktop.
import token from '../../token.json';
import axios from 'axios';
axios.defaults.validateStatus = () => true;
export type TokenCache = {
access_token: string;
refresh_token: string;
expires_at: string;
accounts_server: string;
location: string;
};
export type LeadInput = {
// System-required for Leads per V8 docs:
Last_Name: string;
// Common optional fields — extend as needed using API field API names:
First_Name?: string;
Company?: string;
Email?: string;
Phone?: string;
Mobile?: string;
Lead_Source?: string;
Description?: string;
[apiName: string]: unknown; // allow any additional Lead field
};
export class ZohoCrmClient {
private static token: TokenCache = token;
private readonly clientId = process.env.ZOHO_CLIENT_ID!;
private readonly clientSecret = process.env.ZOHO_CLIENT_SECRET!;
constructor() {
if (!this.clientId || !this.clientSecret) {
throw new Error('Missing required env vars: ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET');
}
}
/** Public: create a Lead record and return the created record id & details */
async createLead(input: LeadInput) {
const { accessToken, apiDomain } = await this.getAccessToken();
const url = `${apiDomain ?? 'https://www.zohoapis.ca'}/crm/v8/Leads`;
const data = { data: [input] };
console.log(`Creating lead for ${input.email} with Zoho CRM...`);
const response = await axios.post(url, data, {
headers: {
Authorization: `Zoho-oauthtoken ${accessToken}`
}
});
if (response.status >= 300) {
// Surface Zoho-style errors if present
const message = response.data?.message ?? `Zoho CRM error ${response.statusText}`;
const code = response.data?.code;
throw new Error(`${code ?? 'ERROR'}: ${message}`);
}
console.dir({ createLeadResponse: response.data }, { depth: null });
const [row] = response.data.data ?? [];
if (row?.status !== 'success') {
const code = row?.code ?? 'UNKNOWN';
const message = row?.message ?? 'Insert lead failed';
throw new Error(`${code}: ${message}`);
}
return {
id: row.details?.id as string,
details: row.details,
raw: row
};
}
/** Internal: get a valid access token, refreshing if needed */
private async getAccessToken(): Promise<{ accessToken: string; apiDomain: string }> {
// TODO check and reuse existing `ZohoCrmClient.token.access_token`
console.log('Refreshing access token...');
const response = await axios.post('https://accounts.zoho.com/oauth/v2/token', null, {
params: {
refresh_token: ZohoCrmClient.token.refresh_token,
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'refresh_token'
}
});
if (response.status >= 300) {
throw new Error(response.statusText);
}
const { access_token, api_domain, expires_in } = response.data;
ZohoCrmClient.token.access_token = access_token;
const now = new Date();
const expiresAtMs = now.getTime() + expires_in * 1000 - 60 * 1000; // 1 minute before expiration
ZohoCrmClient.token.expires_at = new Date(expiresAtMs).toISOString();
return { accessToken: access_token, apiDomain: api_domain };
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment