Skip to main content

Zoho CRM

Bidirectional integration: new/reassigned leads trigger an outbound call; post-call data (call log, note) syncs back to the lead record.

DirectionWhat happens
Inbound — Zoho → SprioA lead is created or updated (e.g. reassigned) and Sprio places an outbound call
Outbound — Sprio → ZohoAfter a call, a Call record and a Note are written onto the lead

Connecting Zoho CRM​

Zoho CRM uses OAuth 2.0 — connect via Integrations → Zoho CRM → Connect, which redirects to GET /api/oauth/zoho/callback. The OAuth state parameter is single-use with a 10-minute expiry; it's the only thing authenticating that callback, so don't reuse an old connect link.

Sprio supports all of Zoho's regional data centers: in, com, eu, com.au, jp, ca, sa (defaults to in). Once connected, Sprio stores the token response's actual API domain (e.g. https://www.zohoapis.in) and uses that directly, rather than guessing it from the data center code.

Access tokens are short-lived (1 hour) and refreshed automatically 5 minutes before expiry — you won't see auth failures from normal token aging.

As soon as the connection completes, Sprio arms the lead-change watch described below — there's no separate "enable webhooks" step.


Inbound: auto-call trigger​

How Sprio watches for lead changes​

Unlike LeadSquared's persistent webhook registration, Zoho's Notifications API uses watch channels that expire — Sprio arms a channel on Leads.create and Leads.edit for 6 days at a time (just under Zoho's own 7-day maximum) and renews it automatically before expiry. A second, independent health check periodically asks Zoho directly whether the channel is still active — this exists because Zoho has, in practice, shown a channel as healthy on its own status API while silently delivering zero notifications for an extended period. If a client mentions "leads have stopped calling" and no code changed, this silent-channel failure mode is the first thing to check.

Both create and edit events are watched deliberately: watching creation alone misses leads that are reassigned to the trigger owner after creation — the same real-world pattern seen with LeadSquared reassignment.

Webhook contract​

POST /api/webhooks/zoho-crm/{connectionId}

Zoho's Notifications payload is minimal — just the changed record's IDs and an echoed token, not the lead's actual field values. Sprio validates the token against the value stored at subscription time; a mismatch returns 401 {"error": "unauthorized"}. On a valid request, Sprio responds 200 {"received": true} immediately and does all real work afterward — Zoho does not need to wait on lead-processing to complete.

Up to 50 changed record IDs can arrive in a single notification (Zoho batches). Each ID is queued individually to a bounded-concurrency worker, not processed inline — a bulk reassignment of hundreds of leads is a normal event this queue is built to absorb without exhausting the database connection pool.

Field resolution​

Because the webhook payload carries only an ID, Sprio always fetches the full lead via GET /crm/v8/Leads/{id} before doing anything else. Phone number is taken from the first non-empty of: Mobile, Phone, Secondary_Phone, Home_Phone.

Owner restriction​

Same model as LeadSquared: trigger_owner_user_id compares against the lead's Owner.id; a mismatch is skipped and logged, not treated as an error.

Call-window scheduling​

Zoho CRM leads support an additional feature not yet on LeadSquared: a configurable calling window (call_window_start / call_window_end, HH:MM in IST, plus an exclude_sunday toggle). A lead that qualifies for a call outside the configured window is scheduled for the next valid time instead of dialed immediately.

Dedup guarantees​

Identical model to LeadSquared:

  • Once per lead per IST calendar day
  • Cross-assistant phone dedup within the org
  • Enforced by database uniqueness constraints, not just pre-checks — a losing race is a clean no-op, not an error

Loop guard​

A lead whose phone number matches one of the org's own platform numbers is never auto-called.

Context passed to the assistant​

Full_Name, First_Name, Last_Name, Company, Email, Lead_Source, Lead_Status, City, State, Description, Designation.


Outbound: post-call sync​

Call log​

POST /crm/v8/Calls
FieldValue
What_IdThe lead's Zoho ID
$se_moduleLeads (required for Zoho to associate the call correctly)
Call_TypeOutbound or Inbound
Call_Start_TimeISO 8601 with +05:30 offset
Call_Duration"HH:MM" string

Zero-duration calls are skipped entirely — Zoho rejects a "00:00" duration with DEPENDENT_MISMATCH. Real calls under a minute are rounded up to "00:01" rather than dropped, since "00:00" isn't accepted even for a genuine short call.

Note​

POST /crm/v8/Leads/{id}/Notes

Note_Content (capped at 32,000 characters) carries the call transcript plus a signed recording link, valid for 30 days.


Follow-up automation: Sunday RNR retry​

Opt-in via settings.sunday_rnr_retry === 'true'. Every Sunday at 9 AM IST, Sprio queries Zoho via COQL (POST /crm/v8/coql) for leads where a configured disposition field (settings.disposition_crm_field, e.g. RNR) matches, and re-queues them for another call attempt. A 20-hour dedup window on metadata.sunday_rnr_retry prevents a lead from being retried twice in the same sweep.

Troubleshooting​

SymptomLikely cause
Leads stopped triggering calls, nothing changed on our sideZoho's watch channel can silently stop delivering while still reporting healthy — check the connection's channel status, not just whether it exists
A reassigned lead doesn't trigger a callConfirm the watch covers both Leads.create and Leads.edit
Call log missing for a very short callExpected if duration rounds to "00:00" before the fix — should now show "00:01"
Note push failsCheck Note_Content length — content over 32,000 characters is rejected