Connect Zoho CRM to NumberGroup: log calls, match leads by phone number, fire workflow rules
Push NumberGroup call events into Zoho CRM — Calls module records, lead/contact matching, workflow-rule triggers, and the Zoho One vs Zoho CRM gotcha to know.
Zoho CRM is the dark horse of UK SME CRMs — popular partly because Zoho One is genuinely good value, partly because it’s the CRM your accountant likely uses. Wiring it to NumberGroup follows the same pattern as the others: webhook receiver, API token, two calls per inbound call.
Foundation: NumberGroup call webhooks explained.
What you’ll have at the end
- Inbound calls land in the Calls module as a Completed Inbound Call.
- Linked to the matching Lead or Contact (by phone).
- Workflow rules on the Calls module fire — so “notify rep on inbound call from VIP” or “create follow-up task after call” just work.
- Duration, direction, DDI dialled all captured.
First — know your data centre
Zoho runs separate data centres for different regions and the API domain differs by region:
- EU customers (most NumberGroup Members):
https://www.zohoapis.eu - UK-specific:
if you signed up before EU migrationhttps://www.zohoapis.com - India / US: different domains
Check your Zoho URL — if you log in at
crm.zoho.eu you use the .eu API. Using the wrong domain returns INVALID_TOKEN with no clear hint.Step 1 — Self-client OAuth credentials
Easiest path for server-to-server:
- Go to api-console.zoho.eu (or
for non-EU)..com - Add Client → Self Client.
- Copy Client ID and Client Secret.
- Generate code → scopes:
,ZohoCRM.modules.calls.ALL
,ZohoCRM.modules.contacts.READ
. Time duration: 10 minutes.ZohoCRM.modules.leads.ALL - Exchange the code for a refresh token immediately:
curl -X POST "https://accounts.zoho.eu/oauth/v2/token" \
-d "code=THE_CODE" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"Save the
refresh_token as a worker secret. Access tokens are short-lived (1 hour); the refresh token mints new ones.Step 2 — Worker code
// numbergroup-zoho-worker.js
const ACCOUNTS = "https://accounts.zoho.eu"; // or .com
const API = "https://www.zohoapis.eu"; // or .com
export default {
async fetch(req, env) {
if (req.method !== "POST") return new Response("ok");
const call = await req.json();
const phone = normaliseUK(call.caller);
const token = await getAccessToken(env);
let who = await findContact(token, phone);
if (!who) who = await findLead(token, phone);
if (!who) who = await createLead(token, phone);
await createCallRecord(token, who, {
caller: phone,
dialled: call.called,
duration: call.duration || 0,
ts: call.ended_at || new Date().toISOString(),
recordingUrl: call.recording_url || null,
});
return new Response("ok");
},
};
function normaliseUK(n) {
if (!n) return null;
n = n.replace(/[^0-9+]/g, "");
if (n.startsWith("+")) return n;
if (n.startsWith("0")) return "+44" + n.slice(1);
return "+" + n;
}
async function getAccessToken(env) {
const cached = await env.KV.get("zoho_token");
if (cached) return cached;
const body = new URLSearchParams({
refresh_token: env.ZOHO_REFRESH_TOKEN,
client_id: env.ZOHO_CLIENT_ID,
client_secret: env.ZOHO_CLIENT_SECRET,
grant_type: "refresh_token",
});
const r = await fetch(`${ACCOUNTS}/oauth/v2/token`, { method: "POST", body });
const j = await r.json();
await env.KV.put("zoho_token", j.access_token, { expirationTtl: 3300 });
return j.access_token;
}
async function zoho(token, path, init = {}) {
return fetch(`${API}/crm/v6${path}`, {
...init,
headers: {
authorization: `Zoho-oauthtoken ${token}`,
"content-type": "application/json",
...(init.headers || {}),
},
});
}
async function findContact(token, phone) {
const r = await zoho(token, `/Contacts/search?phone=${encodeURIComponent(phone)}`);
if (r.status === 204) return null;
const j = await r.json();
return j.data?.[0] ? { module: "Contacts", id: j.data[0].id } : null;
}
async function findLead(token, phone) {
const r = await zoho(token, `/Leads/search?phone=${encodeURIComponent(phone)}`);
if (r.status === 204) return null;
const j = await r.json();
return j.data?.[0] ? { module: "Leads", id: j.data[0].id } : null;
}
async function createLead(token, phone) {
const r = await zoho(token, "/Leads", {
method: "POST",
body: JSON.stringify({
data: [{
Last_Name: `Unknown caller ${phone}`,
Phone: phone,
Lead_Source: "Inbound call",
Company: "Unknown",
}],
}),
});
const j = await r.json();
return { module: "Leads", id: j.data[0].details.id };
}
async function createCallRecord(token, who, c) {
await zoho(token, "/Calls", {
method: "POST",
body: JSON.stringify({
data: [{
Subject: `Inbound call to ${c.dialled}`,
Call_Type: "Inbound",
Call_Start_Time: c.ts,
Call_Duration: Math.round(c.duration / 60), // minutes
Call_Duration_in_seconds: c.duration,
Call_Result: "Completed",
Caller_ID: c.caller,
Description: c.recordingUrl ? `Recording: ${c.recordingUrl}` : "",
Who_Id: who.module === "Contacts" ? who.id : undefined,
$se_module: who.module,
What_Id: who.module === "Leads" ? who.id : undefined,
}],
}),
});
}Step 3 — Wire it up
- Portal → Web Hook Manager → new webhook → URL points to your worker.
- Attach as End of call webhook on each DDI.
Step 4 — Light up Zoho workflow rules
The win with Zoho is that once Calls land in the Calls module, your existing workflow rules just fire. Examples we’ve seen Members set up:
- If
matches a VIP account → email account manager immediately.Caller_ID - If call duration > 5 min → auto-create a follow-up Task for tomorrow.
- If 3+ inbound calls from same number in 7 days → bump Lead Rating to “Hot”.
Zoho-specific gotchas
- Wrong data centre = silent fail. If the API returns
on a fresh token, you’re hitting the wrong domain. SwitchINVALID_TOKEN
↔.eu
..com - Phone search needs an exact match in standard format. If your contacts have
and you search for020 1234 5678
, no hit. Either normalise existing data, or try multiple formats in your worker.+442012345678 - Call_Duration is in minutes (integer). Use
for accurate reporting.Call_Duration_in_seconds - Free plan has no API access. You need at least Standard. (Zoho One always includes it.)
- Rate limits: per-day per-org, scales by edition. Plenty for normal call volume.
Help getting it live
Ring 0800 310 1010 if you get stuck on the OAuth dance — that’s where most Zoho integrations fall over the first time.
Move your phones to us. We'll save you money.
Memorable numbers, unlimited UK numbers, every smart feature and a UK team — all included with Membership at £149/mo. No contract.