> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linkiasoft.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Zoho Desk: message customers when a ticket closes

> Fire a WhatsApp template from a Zoho Desk workflow rule, with no code on your side.

When an agent closes a ticket in Zoho Desk, send the customer a WhatsApp confirmation.
Zoho pushes to us — you don't poll.

This works **without writing any code**, provided the contact's number is stored in a form
we can use. Read [the phone number section](#the-phone-number-is-the-hard-part) before you
start; it's where these integrations actually fail.

## What you need first

<Steps>
  <Step title="An API key scoped to sending">
    **Settings → API Keys**, scoped to `conversations:send`. Nothing more — this key will
    live in your Zoho configuration, where any Zoho admin can read it. See
    [Authentication](/authentication).
  </Step>

  <Step title="Your channel id">
    From [`GET /v1/connect/channels`](/api-reference/channels/list-your-connected-channels). You'll paste
    this into the Zoho webhook as a literal value.
  </Step>

  <Step title="An approved template">
    From [`GET /v1/templates`](/api-reference/templates/list-approved-templates-for-a-channel). Note its exact
    `name`, its `language`, and how many `{{n}}` placeholders its body has.
  </Step>
</Steps>

## Configure the Zoho webhook

In Zoho Desk: **Setup → Automation → Webhooks → Create Webhook**.

| Field   | Value                                                                                         |
| ------- | --------------------------------------------------------------------------------------------- |
| URL     | `https://social.linkiasoft.com/api/v1/conversations/send`                                     |
| Method  | `POST`                                                                                        |
| Headers | `x-api-key: <your key>`, `X-Tenant-Id: <your workspace id>`, `Content-Type: application/json` |

Then add the body parameters:

| Parameter          | Value                                           |
| ------------------ | ----------------------------------------------- |
| `channelId`        | your channel UUID (literal)                     |
| `to`               | `${Contact.Mobile}`                             |
| `contactName`      | `${Contact.First Name}`                         |
| `templateName`     | your template name (literal)                    |
| `templateLanguage` | `ar` (or whatever it was approved under)        |
| `templateParams`   | `${Contact.First Name},${Ticket.Ticket Number}` |

<Note>
  These are **flat** fields, not the nested `template` object shown elsewhere in these docs.
  Zoho's webhook builder can only emit flat key/value pairs, so `/v1/conversations/send`
  accepts `templateName`, `templateLanguage` and `templateParams` at the top level as an
  equivalent form. See [the send endpoint](/api-reference/conversations/send-a-message) for both.

  `templateParams` also accepts a JSON array string — `["Ahmed","4821"]`. Use that form when
  a value can contain a comma, such as a ticket subject or an address.
</Note>

## Trigger it on close

**Setup → Automation → Workflows → Create Rule**

* **Module** — `Tickets`
* **Execute on** — `Edit`, restricted to the `Status` field
* **Criteria** — `Status is Closed` (add `Resolved` too if your team uses both)
* **Action** — the webhook you just created

<Warning>
  Workflow rules are **per-department**. If the team runs several departments, the rule has
  to exist in each one, or tickets closed elsewhere silently send nothing.
</Warning>

## The phone number is the hard part

`to` must be **E.164 without a `+`** — `201001234567`. Zoho stores whatever the agent
typed, which in practice means all of these for the same person:

```
01001234567
0100 123 4567
+20 100 123 4567
(+20) 1001234567
```

Only the third is close to usable. A local-format number **is not rejected** — the send is
accepted and simply never arrives, which is the worst possible failure mode.

Three ways to handle it, best first:

<AccordionGroup>
  <Accordion title="Add a validated WhatsApp Number field (recommended)">
    Add a custom field on the Ticket or Contact layout — `WhatsApp Number` — with a
    validation rule requiring E.164, and make it mandatory before a ticket can close. Then
    use `${Ticket.WhatsApp Number}` for `to`.

    This is the only option that fixes the problem at the source rather than guessing, and
    it eliminates an entire category of "why didn't the customer get the message" tickets.
  </Accordion>

  <Accordion title="Normalize in a Deluge custom function">
    Replace the plain webhook with a custom function on the same workflow rule, and clean
    the number before sending:

    ```javascript theme={null}
    phone = contact.get("mobile").replaceAll(" ","").replaceAll("-","");
    if (phone.startsWith("00")) { phone = phone.removeFirstOccurence("00"); }
    else if (phone.startsWith("0")) { phone = "20" + phone.removeFirstOccurence("0"); }

    payload = Map();
    payload.put("channelId", "<your-channel-uuid>");
    payload.put("to", phone);
    payload.put("templateName", "ticket_resolved");
    payload.put("templateLanguage", "ar");
    payload.put("templateParams", ticket.get("ticketNumber"));

    response = invokeurl [ url: "https://social.linkiasoft.com/api/v1/conversations/send"
                           type: POST
                           parameters: payload.toString()
                           headers: {"Content-Type":"application/json", "x-api-key":"...", "X-Tenant-Id":"..."} ];
    ```

    Hard-coding `20` assumes every customer is in one country. If that isn't true, this
    approach will mangle the exceptions.
  </Accordion>

  <Accordion title="Fall back through several fields">
    Send `${Contact.Mobile}` and fall back to `${Contact.Phone}` or `${Ticket.Phone}` when
    it's empty. This only helps with *missing* numbers, not badly formatted ones — the
    formatting problem remains.
  </Accordion>
</AccordionGroup>

## Two things to watch in production

**Duplicate sends.** A ticket can be closed, reopened, and closed again — each close fires
the rule. Zoho also retries on a non-2xx response. If double-messaging matters, add a
custom checkbox field like `Confirmation Sent`, include `Confirmation Sent is false` in the
rule criteria, and set it as a second workflow action.

**Silent failures.** Zoho records webhook failures in its own log, which nobody reads. Our
API returns `202` as soon as the message is queued, so a `202` does not mean delivered —
check `status` on the message if delivery confirmation matters. See
[step 4 of the quickstart](/quickstart).

## Testing it

Close a real ticket on a contact whose number is your own. If nothing arrives, check in
this order:

1. **Zoho's webhook log** — did the rule fire at all? If not, the criteria or the
   department is wrong.
2. **The response code** — `401` means the key or headers are wrong, `403` means the key
   lacks `conversations:send`, `400` means the payload is malformed and the `message` field
   says how.
3. **The message's `status`** — a `202` followed by `failed` means we accepted it and the
   platform rejected it. `error_reason` will say why; the usual causes are a
   badly-formatted number or a template whose parameter count doesn't match.
