Skip to main content

Zoho

Zoho CRM is a configurable CRM: each org defines modules (Leads, Contacts, Deals, custom modules, etc.) and field API names per module.

Official Zoho doc hubs (for behavior beyond this adapter):

Field API names in Zoho

In Zoho CRM, open Settings → Modules and Fields → [module] → [field]. Use the API name (not the label) in YAML when passing module fields. Mandatory fields depend on the module and org — the customer should confirm what must be set for Leads, Contacts, or any custom module you target.


CRM config (crmConfig)

These values are set once per customer environment (not in public bot YAML). They tune how lookups and defaults behave.

FieldRequiredDefault (if unset)Use
orgIdYesCan be found in customers Zoho URL, with an org prefix (e.g., org1429453822) - getCustomerDetails builds a deep link to the open record in the Zoho CRM UI.
contactModuleNoContactsModule used for getCustomerDetails (COQL FROM clause).
phoneFieldNoPhonePrimary phone field API name on that module.
secondaryPhoneFieldNoMobileSecondary phone field API name (also searched).
defaultRecordNoLeadsDefault module for create-style ops when params.record is omitted.
closeTicketDateFieldNoUsed by closeTicket when stamping a datetime field.
apiDomainNoFrom OAuth metadata or https://www.zohoapis.comZoho API base; some regions/datacenters differ - for example https://www.zohoapis.eu

OAuth for Zoho is configured in the Texter environment; bot YAML does not contain secrets.


Adapter functions

Supported operations in code: getCustomerDetails, customQuery, createRecord, newOpportunity, updateRecord, closeTicket.
newOpportunity and createRecord share the same create implementation — documented below.


getCustomerDetails

Looks up a contact (or configured module row) by matching the chat’s phone against two phone fields on that module.

When it runs: Same pattern as other CRMs: from bot YAML, and the CRM panel in the Texter UI can rely on the same kind of lookup when a chat is opened (phone must be present on the chat).

The adapter runs a COQL SELECT on crmConfig.contactModule (default Contacts). It compares the channel phone in several shapes (digits-only, locally formatted, E.164) against phoneField (default Phone) and secondaryPhoneField (default Mobile).

Basic

  zoho_lookup:
type: func
func_type: crm
func_id: getCustomerDetails
on_complete: known_customer
on_failure: unknown_customer
ParamRequiredNotes
(none)Uses the chat’s E.164 channel phone. If the chat has no phone, the operation fails (on_failure).
fieldsNoArray of extra Zoho field API names to include in the SELECT (strings only). The adapter always requests id, Full_Name, Owner, and both phone fields; fields adds to that list.

Result: On success, crmData includes at least:

  • recordId — Zoho record id.
  • name — from Full_Name.
  • phone — the value from the primary phone field if present, otherwise the secondary.
  • ownerId — from Owner.id (owner user id).
  • deepLink — a URL to the record in Zoho’s UI.
  • Raw fields from the selected row (including any extra columns you asked for in fields) are merged into crmData as returned by Zoho.

Advanced — pull extra columns for later nodes (e.g. custom fields the customer added in Zoho):

  zoho_lookup_with_extras:
type: func
func_type: crm
func_id: getCustomerDetails
params:
fields:
- "Email"
- "Mailing_Street"
- "Custom_Field_API_Name"
on_complete: known_customer
on_failure: unknown_customer

Use only API names that exist on the configured contact module; invalid or unsupported COQL fields will cause the lookup to fail (same as any COQL error).

Lookup and other complex fields: Zoho often returns lookup fields (e.g. Account_Name) as an object with id, name, etc. After getCustomerDetails, you can use expressions like %chat:crmData.Account_Name.id% in later nodes or inside a customQuery WHERE clause. Include those field API names in params.fields when you need them on the contact row.


customQuery

Runs a COQL SELECT you provide. Use this when getCustomerDetails is not enough — e.g. read a field from another module (via id from a lookup on the contact).

ParamRequiredNotes
queryYesFull COQL string passed to Zoho’s COQL API. You can embed %chat:crmData…% (and other supported interpolations) so the query depends on an earlier CRM step.

Result: On success, the first row of the result set is stored under crmData.queryResult: each selected column’s API name becomes a key (e.g. crmData.queryResult.First_Name for SELECT First_Name FROM …). Existing crmData from earlier in the flow is preserved; queryResult is added/updated for this step.

Basic — follow-up read after a contact lookup:

  fetch_related_flag:
type: func
func_type: crm
func_id: customQuery
params:
query: "SELECT Custom_Flag FROM Accounts WHERE id = '%chat:crmData.Account_Name.id%'"
on_complete: branch_on_flag
on_failure: handle_query_error
  branch_on_flag:
type: func
func_type: system
func_id: switchNode
params:
input: "%chat:crmData.queryResult.Custom_Flag%"
cases:
"true": flow_a
"false": flow_b
on_complete: done

If the query returns no rows, the adapter fails — use on_failure or ensure the COQL always matches a row.


createRecord / newOpportunity

Creates a new row in a Zoho module (POST insert records). createRecord and newOpportunity use the same implementation — pick the name that matches your conventions.

When it runs: Most often when you need a Lead, Contact, Task, Case, or custom-module row that does not exist yet (e.g. after on_failure from getCustomerDetails, or to open a task linked to crmData.recordId).

ParamRequiredNotes
recordNo**Module API name (Contacts, Leads, Tasks, Cases, custom module, …). If omitted, uses crmConfig.defaultRecord (default Leads).
dateFieldNoIf set, the adapter sets this Zoho field to the current time (YYYY-MM-DDTHH:mm:ssZ).
(other keys)NoZoho field API names → values on the new record.

Required fields are whatever Zoho enforces for that module in that org — get names and mandatory columns from the customer.

Result: On success, crmData.recordId is set to the new record’s id from Zoho. Other crmData keys are preserved as-is.

Basic — contact with phone and source:

  create_contact:
type: func
func_type: crm
func_id: newOpportunity
params:
record: Contacts
Last_Name: "%chat:title%"
Mobile: '%chat:phone|formatPhone("smart","IL")|replace("-","","g")%'
Lead_Source: "Texter"
on_complete: next_step

Advanced — task on the current contact (Who_Id = contact recordId after getCustomerDetails or after creating the contact):

  open_task_for_contact:
type: func
func_type: crm
func_id: newOpportunity
params:
record: Tasks
dateField: "LastUpdate_WA"
Who_Id: "%chat:crmData.recordId%"
Subject: "Follow up from WhatsApp"
Status: "Not Started"
on_complete: done

updateRecord

Updates an existing row (PUT update records) by recordId.

When it runs: Whenever you already know the Zoho id (usually crmData.recordId after getCustomerDetails) and need to patch fields without creating a new row.

ParamRequiredNotes
(prerequisite)crmData.recordId must be on the chat — set by getCustomerDetails, createRecord, or newOpportunity.
recordIdYesZoho record id — typically %chat:crmData.recordId%.
recordNo**Module API name (Contacts, Leads, Tasks, Cases, custom module, …). If omitted, uses crmConfig.defaultRecord (default Leads).
dateFieldNoIf set, the adapter sets this Zoho field to the current time (YYYY-MM-DDTHH:mm:ssZ).
(other keys)NoZoho field API names → values on the new record.

Result: On success, crmData.recordId is set from the API response (usually the same id). crmData is otherwise merged from the chat as before.

Basic

  tag_contact:
type: func
func_type: crm
func_id: updateRecord
params:
record: Contacts
recordId: "%chat:crmData.recordId%"
Lead_Source: "Texter"
Client_Type: "Business"
on_complete: next_step

Advanced — update “last WhatsApp activity” (or any datetime field) in one call:

  touch_contact_wa:
type: func
func_type: crm
func_id: updateRecord
params:
record: Contacts
recordId: "%chat:crmData.recordId%"
dateField: "LastUpdate_WA"
on_complete: next_step

closeTicket

Same as updateRecord (PUT): update a row (e.g. close a Case) and optionally stamp dateField with the current time.

  • When the chat is resolved by an agent - closeTicket runs with no YAML params. recordId comes from previousBotSession.store.accountId if set, else crmData.recordId. dateField comes from crmConfig.closeTicketDateField when set. record falls back to crmConfig.defaultRecord — set defaultRecord (or CRM config) so that matches the module you close.

  • When you call it from YAML - Put optional record / recordId / dateField overrides, and Zoho fields in params. params are merged last** and win over those defaults.

Requires a recordId on the chat from session or crmData; if both are missing, the op fails ( params.recordId does not satisfy that check).

ParamNotes
(prerequisite)crmData.recordId (or previousBotSession.store.accountId) must be on the chat.
recordModule API name (e.g. Cases).
recordIdOverride; else previousBotSession.store.accountId || crmData.recordId.
dateFieldOverride; else crmConfig.closeTicketDateField.
(other)Same as updateRecord.

Result: Same as updateRecord.

  close_case_in_zoho:
type: func
func_type: crm
func_id: closeTicket
params:
record: Cases
Status: "Closed"
on_complete: done
on_failure: handoff
Bulthaup only

Bulthaup uses a different closeTicket (WhatsApp / transcript). Other customers: generic PUT above.


Zoho onboarding (for Texter Support)

Use this flow to register a Zoho OAuth app and wire client credentials + orgId for a customer. Set apiDomain in CRM config if the org is not on the default US API host (see crmConfig.apiDomain).

Access: Texter support does not have access to the customer’s Zoho org. Creating the API Console app must be done while logged into that customer’s Zoho account — either they create it themselves, or you guide them via AnyDesk (or similar) from their machine.

Step-by-step: Zoho OAuth connection

1. Create a server-based app in the Zoho API Console

Open Zoho API Console (under the customer’s Zoho login) and create a Server-based Applications client.

2. Set the redirect URI (and basic app details)

Use a consistent app name / URL if you like; what matters is Authorized redirect URI:

https://<PROJECT_ID>.texterchat.com/server/auth/oauth/v2/authorize-callback/zoho/default

Replace <PROJECT_ID> with the customer’s Texter project id.

3. Copy Client ID and Client Secret into customer config

After saving, Zoho shows Client ID and Client Secret. Only Gal has permissions to edit OAuth fields in config so ask him to set it up.

4. Authorize from the customer’s Inbox (OAuth settings)

From the customer’s computer (e.g. AnyDesk), open Texter Inbox → Settings → Developers → OAuth. Pick the Zoho OAuth entry and complete the flow: choose scopes (prefer listing all required scopes — if unsure, safer to include more than too few), Save changes, then approve access inside Zoho when prompted. You should end on a success / OK screen.

5. Customer DB — crmConfig fields

Set orgId to the value from the customer’s Zoho CRM URL (open crm.zoho.com while logged in as them — the org id appears in the URL, e.g. org886758394).