Zoho Cliq
Native Zoho Cliq channel for OpenClaw — reply to DMs and channel @mentions as a bot, with streaming previews, cards, and message actions.
Install
openclaw plugins install clawhub:@sprintcx/openclaw-cliq
Zoho Cliq Channel for OpenClaw
Connect your OpenClaw agent to Zoho Cliq — reply to DMs and channel @mentions as a native bot, with streaming previews, cards, buttons, and message actions.
openclaw plugins install clawhub:@sprintcx/openclaw-cliq
Channel plugin · OAuth 2.0 · multi-data-center · MIT · verified live on a real gateway
⚡ Quick start
Get a bot answering DMs in four steps (channel @mention replies add one OAuth step — see Setup guide below):
- Create a Cliq bot — Zoho Cliq → Bots → Create Bot. Note the Bot Unique Name (
botId) and display name. - Get OAuth credentials — Zoho API Console (use your data center's domain) → Self Client → note Client ID + Client Secret.
- Install & configure
bash openclaw plugins install clawhub:@sprintcx/openclaw-cliq openclaw setup # pick "Zoho Cliq" — the wizard writes the account config - Wire the webhook — paste the Deluge handler into the bot's Message/Mention handlers, pointing at
https://<your-gateway>/cliq/webhook.
DM the bot → it answers. To also reply to channel @mentions and stream live edits, add the one-time refresh token in §3c.
Verified live. DMs and channel @mentions both round-trip end to end on a real OpenClaw gateway.
Features
- 💬 Messaging — DMs + channel @mentions via a Deluge webhook, outbound as the bot (DMs via
userids, channel posts viachannelsbyname). Inbound image / file / voice attachments are downloaded and handed to the agent.stop//stop/escinterrupts a running turn. Cliq Form submissions (structured input via the bot's Form Handler) are recognized and routed to the agent with their field values surfaced asFormValues/FormName; the agent can also solicit structured input by rendering a form as a nativepromptcard — a button click re-enters as a structuredFormValuesentry (parameter capture). - ✍️ Rich replies — Markdown → Cliq formatting, live-edit streaming previews, interactive buttons & cards, slash-style commands, reply threading. Opt-in v3 Message Cards (
apiVersion: { channelCard: "v3" }— DM cards already default to v3 viadmPost) addmodern-inline/prompt/pollthemes, supporting-contentslides(table / list / label / image / text blocks),modern-inlinesections(in-card labeled field groups), and athumbnailheader image. - ⚡ Message actions — Edit / delete / react to sent messages from the agent.
- 🔐 OAuth 2.0 —
client_credentialsfor DMs; a user-context refresh token for channel posts / message edits. Works on any Zoho data center. - 🛡️ DM security —
allowlist/pairing/open/disabledpolicies with an approval flow. - 🧩 Per-channel policy — Group admission + per-channel
requireMention, tool policy, and per-sender tool overrides. - 🔁 Reliability — Durable-before-ack ingest, de-dup on redelivery, bot-loop / self-message protection, outbound retry with error classification (parses the v3
{"message":"…"}error envelope). - 🔒 Hardened webhook — Constant-time secret compare, single-header auth, failed-auth rate limiting.
- 🩺 Operations —
openclaw status/channelshealth probe,openclaw directorylookup, plugin doctor, interactive setup wizard, SecretRef credentials, security audit, session binding, multi-account, lifecycle hooks.
Known limitation: the bot can send reactions, but inbound reaction notifications (being told when a user reacts) are not yet possible — the OpenClaw plugin SDK exposes no inbound non-message event hook for external channel plugins. Tracked upstream: openclaw/openclaw#100447.
Setup guide
Everything that must be configured on the Zoho side so the cliq channel plugin can talk to your OpenClaw gateway.
📍 Pick your Zoho data center first. Zoho stores each account in one region, and the domain differs per region. The URLs below use
.com(US) — replace.comwith your data center's domain (.eu,.in,.com.au,.jp,zohocloud.ca,.sa,.com.cn). The plugin's OAuth + API calls default to the EU endpoints; if your account is not on EU, also setoauthBaseandapiBasein the config (see §4). Not sure which region you're on? Check the domain you log into Zoho at — e.g.cliq.zoho.eu→ EU. Full mapping: Data centers.
Prerequisites
- A Zoho account with access to Zoho Cliq and the Zoho API Console.
- A running OpenClaw gateway reachable from the public internet (so Zoho can call the webhook). A reverse proxy, Cloudflare Tunnel, or
ngrokall work for development. - The bot owner must be able to create a bot in Cliq (admin / developer permission).
1. Create a Zoho Cliq Bot
Open the bot builder: click your profile picture (top-right in Zoho Cliq) → under My Cliq choose Bots & Tools.
- In Bots & Tools, open the Bots section.
- Click Create Bot.
- Fill in:
- Bot Name (display name, e.g.
OpenClaw Agent) — this is what users see. - Bot Unique Name (e.g.
openclaw_agent) — this is thebotIdyou will put in the plugin config. Lowercase, underscores, no spaces. - Bot Type: choose Custom Bot (a Deluge-backed bot whose handlers forward to the webhook). A pure "Webhook Bot" is not required — we use a Custom Bot with a Deluge handler that
invokeUrls our endpoint.
- Bot Name (display name, e.g.
- Set the bot's Functional Handlers:
- Mention Handler — fired when the bot is @mentioned in a channel.
- Message Handler — fired when a user DMs the bot directly.
- Publish / Activate the bot (it must be active to receive events).
- Invite the bot into the channel(s) where it should respond to mentions. In a Cliq channel: ⋯ → Bots → add your bot. The bot can always receive DMs without an explicit invite.
The Bot Unique Name you pick here is the
botIdconfig field. The display name isbotName(used for @mention stripping in the agent-visible text).
2. Configure the Webhook
The plugin registers a single HTTP route at POST /cliq/webhook on your OpenClaw gateway. Zoho Cliq's Deluge bot handler must POST every mention / message event to that URL.
-
Pick a strong random secret (e.g.
openssl rand -hex 32) — this becomes yourwebhookSecret. -
Note the public URL of your OpenClaw gateway, e.g.
https://openclaw.example.com. The full webhook URL is:text https://<gateway-host>/cliq/webhookThe route is registered with
auth: "plugin", so no additional gateway-level bearer token is required; thewebhookSecretis verified by the plugin itself via thex-cliq-webhook-secretheader. -
Make sure the gateway host is reachable from the public internet (Zoho's servers POST to it). For local development use a Cloudflare Tunnel /
ngrok/ reverse proxy. -
In the Cliq Bot's Deluge editor (see step 5 below), set the webhook URL and the secret header on every
invokeUrlcall.
3. OAuth / API Credentials
The plugin uses two OAuth grant types, because the client_credentials grant CANNOT obtain a usable token for the ZohoCliq.Channels.UPDATE or ZohoCliq.Messages.UPDATE scopes — Zoho issues a token whose response reports the scope, but the API rejects it on use with {"code":"oauthtoken_scope_invalid"}. So:
- Bot DMs (
/bots/{botId}/message, scopeZohoCliq.Webhooks.CREATE) →client_credentials(the plugin fetches a fresh access token automatically when the cached one expires; no refresh token, no user interaction). - Channel posts (
/channelsbyname/{unique_name}/message, scopeZohoCliq.Channels.UPDATE) and message edits (/chats/{chatId}/messages/{messageId}, scopeZohoCliq.Messages.UPDATE) → a user-context refresh token obtained once via the self-clientauthorization_codeflow. The plugin mints short-lived access tokens from it viagrant_type=refresh_tokenand caches them until they expire (~1h). Without a refresh token, channel replies and live-edit streaming previews will fail withoauthtoken_scope_invalid— DM-only setups keep working.
3a. Create the OAuth client
-
Open the Zoho API Console — use your data center's domain (Data centers). Choose Self Client if you do not already have one for Cliq.
-
Create a Server-based Application (or Self Client) and note:
- Client ID
- Client Secret
-
Your data center's OAuth token endpoint (example uses US
.com):text https://accounts.zoho.com/oauth/v2/tokenThe plugin defaults to the EU endpoint (
https://accounts.zoho.eu). If your account is on another data center, setoauthBase(andapiBase) in the config (§4) to match — see Data centers. -
Copy Client ID and Client Secret — they go into
clientId/clientSecretin the plugin config below.
3b. Consent the scopes
When registering / re-consenting the self-client, request all ten scopes so both the client_credentials (DM) and refresh-token (channel/edit/delete/card/media) paths work:
Each scope's grant is shown in parentheses — client_credentials is fetched automatically; refresh token requires the one-time §3c token.
ZohoCliq.Webhooks.CREATE(client_credentials) — Post bot DMs (the/bots/{botId}/messagesend path).ZohoCliq.Channels.UPDATE(refresh token) — Post bot messages to channels (the/channelsbyname/{unique_name}/messagesend path).ZohoCliq.Channels.CREATE(refresh token) — Post a v3 Message Card to a channel (only when thechannelCardfamily resolves to v3 — the v2 channel card path reusesChannels.UPDATE; opt-in, see §4).ZohoCliq.Channels.READ(client_credentials) — Read channel / chat metadata.ZohoCliq.Users.READ(client_credentials) — Resolve sender user info.ZohoCliq.Messages.UPDATE(refresh token) — Edit a sent message in place (live-edit streaming previews).ZohoCliq.Messages.READ(refresh token) — Read recent chat messages to resolve an inbound file attachment's file id (a Cliq bot Message handler deliversattachmentsas bare file-name strings — the plugin fetches the file message viaGET /api/v2/chats/{chatId}/messagesto recover the downloadable id). Skip it for a text-only bot and inbound images degrade to name-only (no bytes reach the agent); the quote/reply parent-text fetch also uses this scope.ZohoCliq.Messages.DELETE(refresh token) — Delete a sent message via the v3 bulk-delete endpoint (only when thedeletefamily resolves to v3 — the v2 single-message delete reusesMessages.UPDATE; opt-in, see §4).ZohoCliq.messageactions.CREATE(refresh token) — Add / remove message reactions (themessage(action=react)tool).ZohoCliq.Attachments.READ(refresh token) — Download inbound file / image / voice attachments (GET /api/v2/files/{id}) so they reach the agent.
Image analysis requires a vision-capable model. Plugin channels (like Cliq) route inbound images through the runtime's
media-understandingdescribe pipeline, which needs a vision-capable model to produce a text description. If your primary model is text-only (e.g.mimo-v2.5-pro), configure either (a) a vision-capable model as your primary, (b) an explicittools.media.imageprovider, or (c) a vision-capable fallback model the runtime can auto-discover. Without this, the agent receives the image file path but cannot analyze its contents. The turn degrades gracefully (no orphaned placeholder) — but the image content is not described to the agent.
If you previously consented with only the original three scopes, you must re-consent (generate a fresh self-client token) with
ZohoCliq.Channels.UPDATEandZohoCliq.Messages.UPDATEadded — channel replies will be rejected withinvalid_scope/ 401 until you do.ZohoCliq.Messages.DELETEandZohoCliq.Channels.CREATEare only needed when you opt the corresponding family into v3 (delete/channelCard— see §4); the v2 paths reuseMessages.UPDATE/Channels.UPDATErespectively, so if you keep those families on the"v2"default you can skip them. The v3 bot-DM endpoint (thedmPostdefault) uses the sameZohoCliq.Webhooks.CREATEscope as v2 DMs (client_credentials, no extra scope) — though some orgs may additionally requireZohoCliq.BotMessages.CREATE; if yours does, fall back withapiVersion: { dmPost: "v2" }. Reactions (ZohoCliq.messageactions.CREATE) are optional — skip the scope if you don't need thereactaction, and the plugin will simply not advertise reaction support.ZohoCliq.Messages.READis only needed for inbound image / file attachments (resolving a bot-handler file name to a downloadable id) and the quote/reply parent-text fetch — skip it for a text-only bot and those features degrade gracefully. LikewiseZohoCliq.Attachments.READis only needed for inbound media (downloading the resolved images / files / voice a user sends) — skip it for a text-only bot and the plugin degrades to "no media" for those messages.
3c. Obtain the user-context refresh token (required for channel posts + edits)
Channel posts need a user-context token, which client_credentials cannot provide
(Zoho issues a client_credentials token that claims the ZohoCliq.Channels.UPDATE
scope, but the channel API rejects it with oauthtoken_scope_invalid). You get a
user-context token from a refresh token, obtained once via the Self Client's
authorization-code flow. This is a two-step process — a short-lived code that you
exchange for a permanent refresh token.
The default already skips this for bot DM posts. The
dmPostfamily defaults to v3 (see §4): bot DM posts route throughPOST /api/v3/bots/{botId}/messageswith theZohoCliq.Webhooks.CREATEscope obtainable viaclient_credentials, so a refresh token is not required for DMs. The v3 DM endpoint posts as the bot (sender identity preserved — the bot unique name is in the URL path) and sends the recipient key asuserids+sync_message: true, so the response carries the sentmessage_id+chat_id(undermessage_details.<userId>) — what thethinkingplaceholder and DM live-edit-in-place need to edit the reply in place. Some orgs may additionally requireZohoCliq.BotMessages.CREATEfor the v3 bot endpoint; if yours rejects the v3 DM post, restore the v2 path withapiVersion: { dmPost: "v2" }.Channel text posts still need the refresh token by default (
channelPostdefaults to v2 →Channels.UPDATE). OptingapiVersion: { channelPost: "v3" }routes channel text posts throughPOST /api/v3/channelsbyname/{name}/messages(alsoWebhooks.CREATE, so no refresh token) — but v3 channel posts return no message id, so live-edit for channel posts degrades to block-streaming (no win over v2). The message delete family stays v2 by default (Messages.UPDATE); opt{ delete: "v3" }for the v3 bulk-delete endpointDELETE /api/v3/chats/{chatId}/messagess?message_ids=<id>— that scope (Messages.DELETE) still needs a user-context refresh token, so the refresh token is still required for deletes even in v3. Channel card/button posts (in channels), media posts, and message edits still require the refresh token (channel card posts route through the v3 Message Card endpointPOST /api/v3/channels/{name}/messagewith theZohoCliq.Channels.CREATEscope — a user-context scope, same constraint asChannels.UPDATE; media posts stay on v2 indefinitely — v3 has no byte-upload surface, only a public-HTTPS-image Message-Card slide that posts as the user, not the bot; message edits stay on v2 indefinitely — v3 Messages has no single-message edit endpoint). DM card/button posts in v3 route through the v3 "Send a bot message" endpointPOST /api/v3/bots/{botId}/messageswith a top-levelcardfield — the sameWebhooks.CREATEscope as DM text posts (client_credentials, no refresh token needed), posting as the bot and addressing the recipient viauserids(no chat-id resolution needed). If you only need DM text + DM cards, the default already skips this step entirely. SkipMessages.DELETEfrom the scope if you don't use deletes; likewise skipChannels.CREATEif you don't use v3 channel cards. Verify your Zoho org accepts the v3 endpoints before relying on it.
Why only once: only the code is short-lived (10 minutes, single-use). The refresh token you get from it does not expire — it survives gateway restarts and any amount of downtime. You do this once per Cliq org and never again (unless you revoke it).
Step 1 — generate the code (valid 10 minutes):
- In the Zoho API Console (your data center) → your Self Client → tab Generate Code.
- Scope (the Self Client field is comma-separated, no spaces):
text ZohoCliq.Webhooks.CREATE,ZohoCliq.Channels.UPDATE,ZohoCliq.Channels.CREATE,ZohoCliq.Messages.UPDATE,ZohoCliq.Messages.READ,ZohoCliq.Messages.DELETE,ZohoCliq.Channels.READ,ZohoCliq.Users.READ,ZohoCliq.messageactions.CREATE,ZohoCliq.Attachments.READ - Time Duration: 10 minutes. Scope Description: anything (e.g.
openclaw). Pick your portal/org if prompted. - Click Create and copy the code — it looks like
1000.<hex>.<hex>.
Step 2 — exchange the code for a refresh token (do this within the 10 minutes):
The Self Client console gives you a code, not the token. Exchange it against your data
center's token endpoint (the example uses US .com — see Data centers;
no redirect_uri is needed for the self-client flow):
curl -X POST "https://accounts.zoho.com/oauth/v2/token" \
-d "grant_type=authorization_code" \
-d "client_id=<clientId>" \
-d "client_secret=<clientSecret>" \
-d "code=<code from step 1>"
The response is JSON:
{
"access_token": "1000....", // short-lived (~1h) — ignore it
"refresh_token": "1000....", // PERMANENT — this is the one you keep
"scope": "ZohoCliq.Webhooks.CREATE ZohoCliq.Channels.UPDATE ...",
"expires_in": 3600
}
Copy the refresh_token value.
Step 3 — store it: put the refresh_token in the plugin config as refreshToken (see §4).
From then on the plugin mints its own short-lived access tokens from it automatically
(grant_type=refresh_token), forever.
Troubleshooting:
invalid_codeon exchange → the code expired (>10 min) or was already used once. Generate a fresh one.- Channel replies still
oauthtoken_scope_invalid→ therefreshTokenis missing from config, or was minted withoutZohoCliq.Channels.UPDATE. Re-run 3c with the full scope list.channel_not_existson a real channel → the bot is not a participant of that channel (invite it: channel ⋯ → Bots), or you used the channel's display name instead of its unique name (the technical name — e.g. a channel shown as "Finance" may have the unique nameinvest).
If you skip 3c entirely, the plugin still works for bot DMs (the client_credentials
path); only channel @mention replies and live-edit message edits require the refresh token.
4. OpenClaw Configuration
Add the cliq channel to your openclaw.json (or via openclaw setup / the setup wizard's applyAccountConfig step). The required fields are clientId, clientSecret, and botId; botName, webhookSecret, and allowFrom are recommended.
{
"channels": {
"cliq": {
"accounts": {
"default": {
"clientId": "<OAuth client id from step 3a>",
"clientSecret": "<OAuth client secret from step 3a>",
"botId": "openclaw_agent", // Bot Unique Name from step 1
"botName": "OpenClaw Agent", // Bot display name from step 1
"webhookSecret": "<secret from step 2>",
"refreshToken": "<refresh token from step 3c — required for channel posts / edits>",
"allowFrom": ["<zoho user id of each allowed DM sender>"],
"dmPolicy": "allowlist", // "open" | "allowlist" | "pairing" | "disabled"
// Data center: omit both for EU (default). For any other region set both
// to your data center's endpoints — see "Data centers" below.
"oauthBase": "https://accounts.zoho.com",
"apiBase": "https://cliq.zoho.com"
}
}
}
}
}
Every field except the required ones has a sensible default; groups / thinking / pairing are nested objects (see their descriptions).
-
clientId(required) — OAuth client id from the Zoho API Console. -
clientSecret(required) — OAuth client secret (sensitive). -
botId(required) — Bot Unique Name (the path segment in the bot message API). -
botName(recommended) — Bot display name. Used to strip the@botNamemention from the text the agent sees. -
webhookSecret(recommended) — Shared secret the Deluge handler sends in thex-cliq-webhook-secretheader. If unset, the webhook accepts all requests (not recommended). -
refreshToken(recommended) — User-context OAuth refresh token (sensitive). Obtained once via the self-clientauthorization_codeflow (§3c). Required for channel @mention replies and live-edit message edits — without it, those paths fail withoauthtoken_scope_invalid(theclient_credentialsgrant cannot obtain a usable token forZohoCliq.Channels.UPDATE/ZohoCliq.Messages.UPDATE). DM-only setups can leave it unset. -
ackPolicy(optional) — When the webhook acknowledges Cliq relative to the inbound dispatch."after_dispatch"(default) awaits the full dispatch before sending HTTP 200 — a crash mid-dispatch means Cliq never sees the 200 and redelivers (no lost message)."immediate"fires-and-forgets (faster, but a crash between ack and dispatch loses the message). Deluge timeout gotcha: Zoho'sinvokeUrlin the bot Message handler has a ~40 s hard timeout. With the defaultackPolicy: "after_dispatch"the webhook holds the connection until the whole agent turn finishes, so a slow turn (image analysis, cold model) trips Deluge's "The task has been terminated since the API call is taking too long to respond" even though the reply is delivered out-of-band. Operators with slow turns should setackPolicy: "immediate"(fire-and-forget ack; documented lost-message-on-crash trade-off). Pairs naturally withthinking.mode: "placeholder"(the placeholder posts immediately while the agent works). -
allowFrom(optional) — Array of Zoho Cliq user ids allowed to DM the bot (only effective whendmPolicyisallowlistorpairing). -
dmPolicy(optional) — DM admission policy. Default isallowlist(deny by default).pairingstarts the OpenClaw pairing approval flow for unknown senders — by default the sender gets a reply with a pairing code and the bot owner runsopenclaw pairing approve cliq <code>on the CLI; setpairing.notifyOwnerTarget(see thepairingrow below) to instead post an Approve/Deny card to the owner so approval happens inline in Cliq. Accepted values:open,allowlist,pairing,disabled(schema-validated — unknown field names likedmSecurityare rejected). -
groupPolicy(optional) — Group/channel admission policy.openlets the bot respond in any channel it's @mentioned in;allowlist(the effective default once agroupsmap is present) restricts it to channels listed undergroups;disabledignores all group messages. -
groups(optional) — Per-channel config keyed by the Cliq channel unique name. Each entry supportsrequireMention(boolean —falselets the bot respond in that channel without an explicit @mention),ingest(boolean),tools({ allow, alsoAllow, deny }tool policy for that channel), andtoolsBySender(per-sender tool policy overrides keyed bychannel:cliq:<senderId>,id:<senderId>,name:<display>,e164:<phone>,username:<handle>, or*). A*entry applies to any channel not listed explicitly. -
thinking(optional) — Instant acknowledgement / "thinking" placeholder. New installs default to an animated placeholder (thinking.mode: "placeholder",thinking.animate: "dots"— cycles💭 .→💭 ..→💭 ...while the agent works). Setthinking.mode: "off"orthinking.animate: "off"to disable.thinking.modeselects the acknowledgement style:"placeholder"(default — post a lightweight text placeholder, default💭 …, configurable viathinking.text, then edit it in place into the final reply — exactly one message, no duplicate),"card"(post a v3 Message Card status indicator — amodern-inlinecard — instead of plain text, and transition its title through explicit phases as the turn runs: it is first posted with the "thinking" phase titlethinking.thinkingText, default💭 thinking…, then edited in place to the "generating" phase titlethinking.text, defaultGenerating…, right before the agent turn dispatches, and finally edited into the reply text when the reply arrives), or"off"(no extra API call per turn).thinking.animatecycles the placeholder through text frames on an interval while the agent turn runs:"dots"(default —💭 .→💭 ..→💭 ...),"spinner"(braille-spinner frames),"custom"(user-providedthinking.animateFrames), or"off"(static placeholder). OnapiVersion: "v3"the card is a real card posted via the v3 Message Card endpoints (DM viaPOST /api/v3/bots/{botId}/messageswith scopeWebhooks.CREATE, channel viaPOST /api/v3/channels/{name}/messagewith scopeChannels.CREATE); on v2 it degrades to the plain-text placeholder since v2 has no buttonless card. This is the Cliq-appropriate substitute for a typing indicator (Cliq exposes no bot typing API). Any acknowledgement mode is a no-op whenstreaming.previewis"on"(the live-edit path already shows progress) or norefreshTokenis configured (editing the placeholder into the reply needs the user-context token).thinking.failureText(optional) overrides what the placeholder/card becomes when the agent turn ends with no reply (the turn threw, or the model produced no output): when set, the placeholder is edited into that text (e.g.⚠️ No reply generated.); when unset, the untouched placeholder is deleted so no stray indicator lingers. Confirmation gate (card-mode only): setthinking.confirmto gate sensitive inbound actions behind an explicit Confirm / Cancel button card instead of dispatching immediately."off"(default) disables gating;"sensitive"gates only when the cleaned message matches athinking.confirmKeywordsentry (case-insensitive word-boundary match; defaults to a conservative destructive-verb list —delete,drop,reset,wipe,purge, …);"always"gates every turn (apart from abort intents and Confirm-button re-dispatches). When gated, aprompt-theme Message Card titledthinking.confirmText(default⚠️ Confirm action?) withthinking.confirmLabel/thinking.cancelLabelbuttons (defaultsConfirm/Cancel) is posted and the agent turn is held until the user taps a button. Confirm re-posts the original message (prefixed with a sentinel) so the next webhook call dispatches the agent with the gate skipped (no re-prompt loop); Cancel posts a sentinel that short-circuits the turn withthinking.cancelledText(default🚫 Cancelled.) and no agent dispatch. The button clicks arrive as ordinary inbound messages via the bot's Message handler (invoke.bot) — no Cliq Context handler is required. Messages longer than 1500 chars bypass the gate (cannot be safely encoded in the confirm button payload). The gate is a UX guardrail, not a security boundary — the agent's own tool / permission policy still applies to the confirmed action. No new OAuth scope (reuses the card-path scopes). Animated placeholder (issue #86): setthinking.animateto optionally cycle the placeholder through text frames on an interval while the turn runs (Cliq has no native typing indicator — this simulates one via periodic edits), then edit it into the final reply when the reply arrives."off"(default) is a static placeholder;"dots"cycles💭 .→💭 ..→💭 ...;"spinner"cycles braille-spinner frames prefixed with a fixedthinking…label;"custom"cyclesthinking.animateFrames(a string array — needs ≥2 non-empty entries or it degrades to the static placeholder). The interval isthinking.animateIntervalMs(default 1200 ms, hard-floored to 800 ms to protect the Cliq edit rate limit) and the total animation duration is capped (default 60 s — past the cap the animation stops and holds the last frame so a long turn does not hammer the edit endpoint). The animation reuses the existingeditMessagepath (Messages.UPDATE, samerefreshTokenprecondition as the placeholder itself); a failed frame edit stops the animation but never breaks the turn (the reply is still delivered). Only one animation runs per in-flight message — it is stopped the moment the reply (or failure text) arrives so a late frame edit can never clobber the reply. No new OAuth scope. -
welcome(optional) — Welcome message on subscribe. When the Cliq bot Welcome Handler forwards a subscribe event to the webhook (see §5a) andwelcome.enabled === true, the bot posts a configurable greeting DM to the subscriber.welcome.textis used for first-time subscribers andwelcome.textRejoinfor users who unsubscribed and came back; both default to a friendly greeting and support{{firstName}}/{{lastName}}/{{name}}/{{id}}/{{email}}placeholders resolved from the forwardeduserobject. The DM admission policy (dmPolicy/allowFrom) is honored — a denied sender is never greeted, and under thepairingpolicy an un-paired subscriber is skipped (the pairing flow owns their first contact). Defaultenabled: false(opt-in, so no setup gets a surprise greeting). A redelivered subscribe event is deduped so the user is never greeted twice. -
pairing(optional) — Form-driven pairing approval (only effective whendmPolicyispairing). By default an unknown sender's pairing code must be approved on the CLI (openclaw pairing approve cliq <code>). Setpairing.notifyOwnerTargetto a Cliq route target —cliq:user:<zohoUserId>/user:<zohoUserId>/cliq:channel:<uniqueName>/channel:<uniqueName>(a bare string is treated as a DM user id) — and the pairing flow additionally posts an approval prompt card there (aprompt-theme Message Card with Approve/Denyinvoke.botbuttons) carrying the sender id + pairing code. The owner taps Approve to admit the sender (the plugin calls the SDK'sapproveChannelPairingCode, writing the sender to the channel allowFrom store, and DMs the sender that they were approved) or Deny to replydeniedOwnerText(the pending request is left in place; the sender is re-challenged idempotently if they message again). The CLI step keeps working alongside the card. RequiresbotId(the v3invoke.botbutton renderer drops buttons without one); when absent ornotifyOwnerTargetis unset, the card is skipped and only the CLI path is used. The button click arrives as an ordinary inbound message and is short-circuited before the mention / admission gates (so the owner need not be on the allowlist to approve). Optional overrides:approveLabel/denyLabel(button labels, defaultApprove/Deny),approvalTitle(card title, default🔐 Pairing request),approvedOwnerText(owner reply on approve, default✅ Approved.),deniedOwnerText(owner reply on deny, default🚫 Denied.). No new OAuth scope — the approval card reuses the same card-path scopes (Webhooks.CREATEfor DM cards viaclient_credentials;Channels.UPDATEon v2 /Channels.CREATEon v3 for channel cards) the existingmessage(action=send, buttons=…)path uses. -
oauthBase(optional) — OAuth base URL for your Zoho data center. Defaults to the EU endpointhttps://accounts.zoho.eu. Set it (together withapiBase) when your account is not on EU — see Data centers. -
apiBase(optional) — Cliq REST API base URL for your Zoho data center. Defaults to the EU endpointhttps://cliq.zoho.eu. Set it (together withoauthBase) when your account is not on EU. -
apiVersion(optional) — REST API generation for the channel text post, bot DM post, message delete, and channel card/button post families. Accepts EITHER a string global override OR a per-family object:- a string
"v2"forces ALL migratable families to the verified-live v2 endpoints;"v3"forces all to v3. - an object
{ dmPost?, channelPost?, channelCard?, delete? }overrides per-family; any family left unset falls back to the built-in default.
Built-in per-family defaults (the sole family where v3 is strictly better flips to v3; the rest stay v2). Omitting
apiVersionentirely yields these defaults — you no longer need to setapiVersion: "v3"for the DM placeholder / live-edit to work (a previous manifestdefault: "v2"bug silently forced all families to v2; fixed in issue #86).dmPost→"v3"(the win). The v3 "Send a bot message" endpointPOST /api/v3/bots/{botId}/messages(scopeZohoCliq.Webhooks.CREATEviaclient_credentials— no refresh token needed, posts as the bot) sends the recipient key asuserids(v2-style, NO underscore — auser_idskey is rejected withextra_key_found) andsync_message: true, so the response carries the sentmessage_id+chat_idundermessage_details.<userId>(URL-encoded — decoded once and the v2 edit URL re-encodes exactly once). That id is what thethinking.mode: "placeholder"acknowledgement and DM live-edit-in-place need to edit the reply in place; the v2 DM endpoint returns no message id, so on v2 DMs those features leave an orphaned💭 …. Some orgs may additionally require theZohoCliq.BotMessages.CREATEscope for the v3 bot endpoint — if your org rejects the v3 DM post, restore the v2 path withapiVersion: { dmPost: "v2" }(or the string"v2").channelPost→"v2". v3 (POST /api/v3/channelsbyname/{name}/messages,Webhooks.CREATE) posts as the bot but returns no message id (live-edit for channel posts degrades to block-streaming — no win over v2). Flip to"v3"only after verifying the win live.channelCard→"v2". The v3 Message Card channel endpointPOST /api/v3/channels/{name}/message(note:channels, notchannelsbyname, singularmessage; scopeZohoCliq.Channels.CREATE, refresh-token grant) has nobot_unique_namequery param, so a v3 channel card posts as the authenticated user (not the bot) — a sender-identity regression. v2 channel cards (channelsbyname/{name}/message?bot_unique_name=,Channels.UPDATE) post as the bot. The v3 path renders amodern-inlineMessage Card body whosethemeis selected by the card sender (modern-inlinefor agent-emitted presentations and themessage(action=send, buttons=[...])tool; themessage(action=send, slides=[...])tool attaches structuredtable/list/label/images/textsupporting-content blocks via the theme-independent top-levelslidesarray; themessage(action=send, thumbnail="https://…", sections=[{ title?, fields: [{ title, value }] }])tool attaches amodern-inline-only headerthumbnailimage (HTTPS URL) and in-card labeled fieldsections(both ignored forprompt/poll);promptfor the slash-command quick-reply buttons emitted by the/modelsand/modelmenus — a focused quick-reply card with a title + 1–5 action buttons, no sections;pollfor voting cards emitted by themessage(action=send, theme="poll", pollOptions=[...])tool — a title + 2–10 plain-text options, no buttons; Cliq counts votes natively, so a poll does NOT post anything back to the bot). DM card/button posts in v3 route through the samePOST /api/v3/bots/{botId}/messagesendpoint as v3 DM text posts, with a top-levelcardfield — sameWebhooks.CREATEscope (client_credentials, no refresh token), posting as the bot and addressing the recipient viauserids; thepolltheme works for DM cards too.delete→"v2". The v3 bulk-delete endpointDELETE /api/v3/chats/{chatId}/messagess?message_ids=<id>(a 1-element delete-multiple call; scopeZohoCliq.Messages.DELETE, refresh-token grant) offers no functional win over the v2 single-message delete (Messages.UPDATE, refresh-token grant). The v3 delete response is a per-messagemessage.delete_resultlist parsed into a boolean.
Locked families (message edit, reactions, media posts, directory listing, file download, channel-chat-id resolution, message list) stay on
/api/v2/...regardless ofapiVersion— v3 has no endpoint for them (confirmed against the v3 REST docs). A v3-posted DM is edited via the v2 edit endpoint (PUT /api/v2/chats/{chatId}/messages/{messageId},Messages.UPDATE) — that hybrid is intended. Per-account overrides supported (one account can pilot a different family mix).ay on v2 regardless — v3 Messages has no single-message edit or get endpoint (only delete-multiple, post, forward, search), so edit/list stay v2 indefinitely. v3 channel posts return no message id (live-edit for channel posts degrades to block-streaming); v3 DMs withsync_message: trueDO return the message id. Per-account overrides supported (one account can pilot v3 while others stay on v2). - a string
Group/channel identity: the inbound path sets the OpenClaw From context field to cliq:group:<channelUniqueName> for group messages (and fills GroupChannel/GroupSubject with the display name as a fallback), so the groups adapter resolves per-channel requireMention and tool policy by channel unique name. Keys are matched case-insensitively.
Gateway reachability: the host running the OpenClaw gateway must be reachable from the public internet at https://<gateway-host>/cliq/webhook. If you run the gateway behind a reverse proxy / Cloudflare Tunnel, make sure TLS termination and the Host header are preserved.
5. Deluge Webhook Handler
The Cliq bot must forward every mention / message event to the OpenClaw webhook. Paste this Deluge script into the bot's Mention Handler and Message Handler functions in the Cliq Bot editor.
Where to find them: in the Cliq Bot editor open Edit Handlers, then click Edit Code on Message Handler (DMs) and Mention Handler (channel @mentions) — the two arrowed below.
// === Configuration (set these once) ===
// Public URL of your gateway's /cliq/webhook route. If you expose the gateway
// port directly (no reverse proxy / TLS) this is http://<host>:18789/cliq/webhook.
webhookUrl = "https://<gateway-host>/cliq/webhook";
webhookSecret = "<the same secret you set as webhookSecret in openclaw.json>";
// Cliq provides `message`, `user`, and `chat` in the Message/Mention handler
// scope. The plugin's parser accepts these Cliq objects as-is (it tolerates the
// different chat/channel key variants), so just forward them directly.
// Forward `attachments` too (issue #84): a Cliq bot Message handler receives
// the file names a user attached as a separate `attachments` argument (bare
// file-name strings — no id / MIME); without forwarding them, a caption-less
// image is rejected as `invalid payload` and an image with a caption dispatches
// but the file never reaches the agent. The plugin resolves the file id from
// the chat-messages list when a `refreshToken` is configured (§3c). The
// argument is absent for text-only messages (Deluge passes null), so guard it.
payload = Map();
payload.put("handler", "message"); // <-- use "mention" in the Mention Handler
payload.put("message", message);
payload.put("user", user);
payload.put("chat", chat);
if (attachments != null) {
payload.put("attachments", attachments);
}
// Auth + content type. The secret header is REQUIRED when webhookSecret is set
// in openclaw.json; Content-Type MUST be application/json.
headers = Map();
headers.put("Content-Type", "application/json");
headers.put("x-cliq-webhook-secret", webhookSecret);
// POST to OpenClaw as raw JSON. Use `body:` (NOT `parameters:`) — see note below.
invoke_response = invokeUrl
[
url : webhookUrl
type : POST
body : payload.toString()
headers: headers
];
// The reply is delivered by the OpenClaw gateway via the Cliq bot API, so the
// handler itself returns an empty response.
response = Map();
return response;
This is the same script for both handlers — the only difference is the
handlervalue:"message"in the Message Handler (DMs) and"mention"in the Mention Handler (channel/group @mentions). Group vs DM is detected automatically from the forwardedchatobject, so no extra mapping is needed.
Do not use
parameters: payload.toString(). In Deluge,invokeUrl'sparameters:key serializes the value as form-urlencoded data (handler=mention&message=...), which is not the JSON body this plugin expects — the gateway returns400 Unexpected token 'h', "handler=me"... is not valid JSON. Always usebody:together with theContent-Type: application/jsonheader shown above.
5a. Welcome Handler (optional)
The Cliq bot Welcome Handler fires when a user subscribes (or re-subscribes) to the bot. To greet new subscribers from OpenClaw config rather than hard-coding the message in Deluge, paste this script into the bot's Welcome Handler function (in the Cliq Bot editor → Edit Handlers → Edit Code on Welcome Handler). It forwards the subscribe event to the same /cliq/webhook endpoint the Message/Mention handlers use, with handler: "welcome" and Cliq's newuser boolean:
// === Configuration (set these once — same values as §5) ===
webhookUrl = "https://<gateway-host>/cliq/webhook";
webhookSecret = "<the same secret you set as webhookSecret in openclaw.json>";
payload = Map();
payload.put("handler", "welcome");
payload.put("user", user);
payload.put("newuser", newuser);
headers = Map();
headers.put("Content-Type", "application/json");
headers.put("x-cliq-webhook-secret", webhookSecret);
invokeUrl
[
url : webhookUrl
type : POST
body : payload.toString()
headers: headers
];
// The greeting is delivered by the OpenClaw gateway via the Cliq bot API,
// so the handler itself returns an empty response.
response = Map();
return response;
When welcome.enabled === true in the channel config, the gateway posts the configured greeting DM to the subscriber (see the welcome row in §4). The event is always acknowledged so Cliq does not redeliver it; a redelivery is deduped by subscriber id so the user is never greeted twice. The dmPolicy / allowFrom gate is honored — a denied sender is never greeted. Without this handler, or with welcome.enabled === false (the default), subscribe events are simply not consumed by the plugin.
5b. Form Handler (optional — structured input)
Zoho Cliq's platform Forms let you define a structured form (text / number / dropdown / date / … fields) that a user fills out from the bot's command surface. When a user submits a form, the bot's Form Handler Deluge script fires and can forward the submitted values to the OpenClaw webhook so the agent receives them as structured input rather than free text — useful for approval / collection flows (pairing approval, parameter capture) instead of asking the user to type free-text answers.
Paste this script into the bot's Form Handler function (in the Cliq Bot editor → Edit Handlers → Edit Code on Form Handler) — it forwards to the same /cliq/webhook endpoint the other handlers use:
// === Configuration (set these once — same values as §5) ===
webhookUrl = "https://<gateway-host>/cliq/webhook";
webhookSecret = "<the same secret you set as webhookSecret in openclaw.json>";
// Cliq passes the submitted values to the Form Handler scope as the `form`
// object's fields (each named after your form field). Read them out and
// forward them as a `values` map the plugin can parse.
payload = Map();
payload.put("handler", "form");
payload.put("form", { "name": "approval_request" }); // your form's name
payload.put("user", user);
payload.put("chat", chat);
// Build the submitted-values map. Replace the keys with your own form's
// field names; Cliq passes each field value as a Deluge variable named after
// the field. Example fields: approver, priority, reason.
values = Map();
values.put("approver", approver);
values.put("priority", priority);
values.put("reason", reason);
payload.put("values", values);
headers = Map();
headers.put("Content-Type", "application/json");
headers.put("x-cliq-webhook-secret", webhookSecret);
invokeUrl
[
url : webhookUrl
type : POST
body : payload.toString()
headers: headers
];
// The reply is delivered by the OpenClaw gateway via the Cliq bot API,
// so the handler itself returns an empty response.
response = Map();
return response;
When a form submission arrives, the plugin synthesizes the agent's message body from the submitted values:
Form: approval_request
approver: alice@corp.com
priority: High
reason: prod deploy gate
The raw structured values are ALSO surfaced on the inbound context as FormValues (a string-keyed map) and FormName (the form's display name), so an agent tool or downstream flow can read them as structured data rather than parsing the body text. A form submission is treated as a directed action at the bot — a group form submission is admitted without a separate @mention (the same way a reply to the bot is). DM admission (dmPolicy / allowFrom) and self-message / dedupe guards apply unchanged. A form submission whose every field is empty is dropped (no agent-readable content). No new OAuth scope is required — the Form Handler is a bot handler that posts to the webhook over the same x-cliq-webhook-secret-authenticated transport as Message / Mention / Welcome. There is no separate opt-in config field — if no form is wired up, no form submissions arrive.
5c. Agent-rendered forms (outbound structured input)
In addition to the inbound Form Handler above, the agent can solicit structured input at runtime by rendering a form as a native Cliq prompt card with a button per option — the portable equivalent of a Cliq platform Form, emitted on demand. The agent calls the shared message tool with a form param:
message(action=send, to="cliq:user:u-1", form={
title: "Which priority?",
fields: [
{ name: "priority", label: "Priority", type: "select",
options: [{ label: "High", value: "high" }, { label: "Low", value: "low" }] },
{ name: "reason", type: "text", placeholder: "brief justification" }
]
})
Rendering rules:
- Each
selectfield with ≥2 options becomes its ownpromptcard (a button per option, capped at 5; extras listed in the card body). Tapping a button posts a__cliq_form__ <fieldName>=<value>sentinel payload back to the bot, which the plugin recognizes and surfaces on the inbound context as a structuredFormValuesentry ({ <fieldName>: <value> }) — so an agent-posted form's answers re-enter as structured params for a tool call, not plain text. The agent envelope body is the clean<fieldName>: <value>rendering (sentinel stripped). A button click is a directed action at the bot, so a group form response is admitted without a separate @mention. text/numberfields (and optionlessselectfields) fold into a singlemodern-inlinesummary card posted before the prompt cards, listing each as a question with areply with <name>: <value>hint. These free-text replies are not sentinel-prefixed and re-enter as ordinary message text (the agent reads them as typed answers); only prompt-card button clicks are structured.- An optional
messageparam prefixes the first card's text as extra context (instructions, context, etc.). - A degenerate form (no viable fields) returns an error so the agent can correct and retry.
No new OAuth scope — prompt cards reuse the same card-path scopes the existing message(action=send, buttons=…) path uses (ZohoCliq.Webhooks.CREATE for DM cards via client_credentials; ZohoCliq.Channels.UPDATE on v2 / ZohoCliq.Channels.CREATE on v3 for channel cards via the refresh-token grant). On v2 the prompt theme is ignored and the buttons render as a standard v2 bot-message buttons array; on v3 (apiVersion: "v3") a real prompt-theme Message Card is posted. No config field — the form param is always available on a configured account.
Payload format reference
The plugin parses the JSON payload posted by the Deluge handler. The canonical shape is:
{
"handler": "mention", // "mention" | "message"
"message": { "text": "hi", "id": "msg_123", "time": "2026-07-04T10:00:00Z" },
"user": { "id": "12345", "name": "Jane Doe", "email_id": "jane@example.com" },
"chat": { "id": "cl_abc", "type": "channel", "title": "Engineering" },
"channel": { "id": "ch_1", "name": "engineering", "unique_name": "engineering" },
"mentions": [ { "id": "openclaw_agent", "name": "OpenClaw Agent", "type": "bot" } ]
}
Notes (the parser is tolerant):
messagemay be a plain string instead of{ text, id, time }.- A wrapped
paramsobject ({ params: { message, user, channel } }) is also accepted. - Group vs DM detection:
chat.type === "channel"(or the presence ofchannel.*fields) marks a group; otherwise the message is treated as a DM. - The
x-cliq-webhook-secretheader is checked against the configuredwebhookSecret. The plugin also acceptsx-webhook-secretorAuthorization: Bearer <secret>for convenience. - Form submissions (see §5b): a payload with
handler: "form"and/or a non-emptyvaluesobject (also accepted underform.values/form_data/formvalues, including inside aparamswrapper) is recognized as a Cliq Form submission; the submitted field values synthesize the agent body and are surfaced asFormValues/FormNameon the inbound context. - Agent-rendered form button clicks (see §5c): a prompt-card button posts
__cliq_form__ <fieldName>=<value>as the message text; the plugin recognizes the sentinel and surfaces the answer as aFormValuesentry ({ <fieldName>: <value> }) on the inbound context (structured params for a tool call), with the clean<fieldName>: <value>rendering as the agent body.
Inbound quote / reply context
When a user replies to or quotes a message in Cliq, the Deluge message handler receives the new message — Cliq does not automatically attach the quoted message's text to the bot. The plugin surfaces the referenced message to the agent from whatever the handler forwards:
message.reply_to(string message id) — the documented Cliq shape. The plugin carries the parent message id into the inbound context.parent/quoted/parent_message/quoted_message/reply_to_message(object at the payload root, or undermessage) — the full parent message{ id, text, sender: { id, name } }. When the handler forwards this, the agent sees the quoted text + sender directly.
When only the parent id is present (the default message.reply_to shape) and a user-context refreshToken is configured, the plugin best-effort fetches the parent message text via GET /api/v2/chats/{chatId}/messages and prepends it to the agent envelope as:
↩ Replying to <senderName>:
> <quoted text>
<the user's message>
A failed or empty fetch degrades to "no quote text" and never breaks the turn.
A reply to the bot in a group is also admitted as an implicit mention (the reply_to_bot / quoted_bot gating kinds) — the user does not need to re-@mention the bot when replying to one of its messages.
Forwarding the parent object is optional. Without it the plugin still carries the parent message id; it only cannot show the quoted text unless a
refreshTokenis configured (so the plugin can fetch it). If your Deluge handler can resolve the parent message (e.g. via the Cliq RESTGET /chats/{CHAT_ID}/messages/{MESSAGE_ID}endpoint), add it underparent(orquoted) so the agent sees the quote even in DM-only setups with norefreshToken.
Stop / abort the running turn
A user can interrupt a running agent turn by sending a stop intent — stop, /stop, esc, or a common localized equivalent (halt, arrête, 停止, стоп, …). When the plugin recognizes the intent it marks the turn as an authorized command, and the OpenClaw runtime's fast-abort path cancels the in-flight run for that session (cancelSession + run-target abort), clears any queued follow-ups, stops spawned sub-agents, and replies with the canonical acknowledgement (⚙️ Agent was aborted.) in the same chat — instead of queueing another agent turn behind the one still running. No extra config, scope, or Deluge wiring is required; the trigger set is the shared one every OpenClaw channel uses.
- In a DM, any stop intent aborts the running turn (DMs are always directed at the bot).
- In a channel, the user must
@mentionthe bot (@bot stop) so the abort is admitted under the same mention gate as a normal reply — a barestopin the channel is treated as room chatter and ignored.
Verification
After the steps above, send a test message:
- DM the bot in Cliq (if
dmPolicyisallowlist, make sure your Zoho user id is inallowFrom). - @mention the bot in a channel it was invited to.
Both should trigger a POST /cliq/webhook on your gateway (visible in the gateway logs) and an agent reply in the same chat. If nothing arrives, check:
- The bot is active/published in Cliq.
- The Deluge handler is saved and the webhook URL / secret are correct.
- The gateway host is reachable from the public internet (curl
https://<gateway-host>/cliq/webhookfrom an external host — a405 Method Not Allowedon GET means the route is live). - The OAuth client has all the scopes from step 3b (including
ZohoCliq.Channels.UPDATEfor channel replies), andoauthBase/apiBasematch your data center (the plugin defaults to EU — see Data centers). For channel @mention replies and message edits,refreshTokenfrom step 3c must be set — otherwise those paths fail withoauthtoken_scope_invalid.
Smoke testing with curl
You can verify the webhook route and the expected JSON body shape independently of Zoho Cliq. Replace <gateway-host> and <secret> with your values:
curl -i -X POST 'https://<gateway-host>/cliq/webhook' \
-H 'Content-Type: application/json' \
-H 'x-cliq-webhook-secret: <secret>' \
--data '{
"handler": "message",
"message": { "text": "hello from curl", "id": "smoke_1" },
"user": { "id": "smoke-user", "name": "Smoke Tester" },
"chat": { "id": "smoke-chat", "type": "channel", "title": "Smoke" }
}'
Expected response (the webhook acknowledges receipt synchronously and dispatches asynchronously):
HTTP/2 200
content-type: application/json
{"status":"received"}
200 {"status":"received"}— the route is live, the secret matched, and the body parsed as JSON. The agent reply (if any) is delivered asynchronously to the chat id you supplied.401 unauthorized— thex-cliq-webhook-secretheader did not matchwebhookSecret.400 ... is not valid JSON— the body was not JSON (e.g. you usedparameters:in Deluge, orContent-Typewasapplication/x-www-form-urlencoded). Re-check the Deluge handler in §5.503 cliq not configured— the channel account is not configured inopenclaw.json(see §4).
Data centers
Zoho stores each account in a single regional data center, and the API / OAuth
domain differs per region (accounts are DC-exclusive — a .eu account cannot
authenticate against .com). The plugin defaults to the EU endpoints; for any
other region, set oauthBase and apiBase in the config (§4)
to the values below, and use the matching domain for the API Console and token
URLs throughout the setup guide.
| Region | Domain | oauthBase | apiBase |
|---|---|---|---|
| Europe (plugin default) | .eu | https://accounts.zoho.eu | https://cliq.zoho.eu |
| United States | .com | https://accounts.zoho.com | https://cliq.zoho.com |
| India | .in | https://accounts.zoho.in | https://cliq.zoho.in |
| Australia | .com.au | https://accounts.zoho.com.au | https://cliq.zoho.com.au |
| Japan | .jp | https://accounts.zoho.jp | https://cliq.zoho.jp |
| Canada | zohocloud.ca | https://accounts.zohocloud.ca | https://cliq.zohocloud.ca |
| Saudi Arabia | .sa | https://accounts.zoho.sa | https://cliq.zoho.sa |
| China | .com.cn | https://accounts.zoho.com.cn | https://cliq.zoho.com.cn |
Which region am I on? Check the domain you log into Zoho at (e.g. cliq.zoho.in
→ India), or read the api_domain / location value Zoho returns during the
OAuth flow.
Auto-detection. You do not have to get the region right on the first try:
- The setup wizard (
openclaw configure) prompts for your Zoho data center first and writesoauthBase+apiBasetogether from the region table above (EU is the default; a re-run reuses your existing region so a non-EU account is never silently reset to EU). The printed API Console URL matches the region you pick. - After the first successful OAuth token exchange, the plugin reads the
api_domainfield Zoho returns in the token response and, when it indicates a region that disagrees with the configuredapiBase, self-correctsapiBaseto the matchingcliq.zoho.<tld>(the rawzohoapishost is mapped back to the Cliq host — never used directly) and logs one warning.oauthBaseis left unchanged: a wrongoauthBasefails before anyapi_domainis returned, so it cannot self-heal — set it via the wizard or the config table above. openclaw doctorwarns when only one ofoauthBase/apiBaseis set (the other defaults to EU, splitting OAuth + REST across regions) or when the two point at different regions (a likely copy-paste mistake).- A Zoho auth failure (
invalid_client/oauthtoken_scope_invalid/ 4xx auth) surfaces averify your Zoho data centerhint pointing back here. This includes v3 endpoints (apiVersion: "v3"), whose{"message":"…"}error envelope is parsed so the auth-failure patterns match the extracted message text (e.g. a v3 401 "…invalid AuthToken." triggers the same hint).
Example — a US-based account (.com). EU accounts can omit both fields:
{
"channels": {
"cliq": {
"accounts": {
"default": {
"clientId": "<from the .com API Console>",
"clientSecret": "<from the .com API Console>",
"botId": "openclaw_agent",
"oauthBase": "https://accounts.zoho.com",
"apiBase": "https://cliq.zoho.com"
}
}
}
}
}
Contributing
Bug reports, feature requests, and pull requests are welcome — see CONTRIBUTING.md for local development, conventions, and the PR flow, and SECURITY.md for private vulnerability reporting. Release and ClawHub-publish steps live in RELEASING.md; the version history is in CHANGELOG.md.
Development
This plugin is developed iteratively by an autonomous coding agent (OpenCode via GitHub Actions). See AGENTS.md for project context and conventions, and ROADMAP.md for the open worklist / feature-parity target. The coding-agent workflow only runs for issues opened by repo maintainers (owner / member / collaborator) — a public issue will not trigger it.
License
MIT
