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