Zoho CRM
Bidirectional integration: new/reassigned leads trigger an outbound call; post-call data (call log, note) syncs back to the lead record.
| Direction | What happens |
|---|---|
| Inbound — Zoho → Sprio | A lead is created or updated (e.g. reassigned) and Sprio places an outbound call |
| Outbound — Sprio → Zoho | After 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
| Field | Value |
|---|---|
What_Id | The lead's Zoho ID |
$se_module | Leads (required for Zoho to associate the call correctly) |
Call_Type | Outbound or Inbound |
Call_Start_Time | ISO 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
| Symptom | Likely cause |
|---|---|
| Leads stopped triggering calls, nothing changed on our side | Zoho'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 call | Confirm the watch covers both Leads.create and Leads.edit |
| Call log missing for a very short call | Expected if duration rounds to "00:00" before the fix — should now show "00:01" |
| Note push fails | Check Note_Content length — content over 32,000 characters is rejected |