### Zoho Desk — orgs, departments, tickets vs threads, and offset paging (read before writing)

Desk looks like the other services and behaves differently in four ways that cause most failures: the org goes in a **header**, almost everything is scoped to a **department**, paging is **offset-based** (`from`/`limit`, not page/per-page), and a customer reply is a **thread**, not a ticket update.

#### The org is a header, not a query param

```bash
zone api desk GET /organizations              # or: zone desk org list
zone ctx desk orgId=<id>                      # injected as the orgId HEADER on every later call
zone desk org accessible                      # orgs this agent can actually reach
```

Every endpoint except `/organizations` requires it. Missing it is an auth-shaped error, not a 404 — set the ctx rather than retrying.

#### Departments scope nearly everything

```bash
zone desk department list --toon              # id, name, isEnabled
zone desk department mine                     # the departments this agent belongs to
zone desk ticket list --department-id <id> --limit 50
```

On reads `--department-id` filters; on much of the settings surface it is **required** (`view list --module tickets --department-id <id>`, `sla list --department-id <id>`, `blueprint reorder`, `emailtemplate placeholders`). When a settings call complains about a missing parameter, a department id is usually what it wants.

#### Paging is offset-based — the biggest difference

There is no `page`/`per_page` and no `page_context`. Lists take `--from` (a **1-based** start index) and `--limit` (how many):

```bash
zone desk ticket list --from 1   --limit 100 --toon
zone desk ticket list --from 101 --limit 100 --toon      # next page
```

Walk it by adding `limit` to `from` until a response returns fewer rows than you asked for. `--limit` has a per-endpoint maximum; ask for too many and Desk clamps or rejects, so keep pages modest and check `zone desk <group> <name> --help`. An empty result can come back as HTTP **204 with no body** rather than an empty array — treat 204 as "no rows", not as a failure.

Related data is opt-in with `--include` (for example `contact`, `assignee`, `department`, `team`, `products`) instead of extra calls. Sorting is `--sort-by`, with a leading `-` for descending on most endpoints.

#### Reading and writing tickets

```bash
zone desk ticket get <id> --include contacts,assignee,department
zone desk ticket create --data '{"subject":"Printer offline","departmentId":"<id>","contactId":"<id>","description":"…","priority":"High","channel":"Email","status":"Open"}'
zone desk ticket update <id> --data '{"priority":"Urgent","assigneeId":"<agentId>"}'      # PATCH — send only what changes
zone desk ticket update-many --data '{"ids":["…","…"],"fieldName":"status","fieldValue":"Closed"}'
zone desk ticket search --search "printer" --limit 20
zone desk ticket count --view-id <cvId>
```

- Updates are **PATCH**: send only the fields you are changing. A full-record PUT is not how Desk works.
- A write body **is the record** — no `{"data":[…]}` envelope. One call, one ticket, except the explicit `update-many` / `updateMany` endpoints.
- `status` is a per-department string ("Open", "Closed", or any custom status). Its coarse bucket is `statusType` (`Open` / `Closed` / `On Hold`) — read `statusType` when you want "is this still open", and write `status` with a value the department actually has.
- Ids (`departmentId`, `contactId`, `assigneeId`, ids in responses) are long numeric **strings**. Keep them as strings.
- Discover fields per module with `zone desk field list --module tickets --department-id <id>`; custom fields ride in the payload's `cf` object keyed by the field's API name.

#### Deleting is a two-step trash

```bash
zone desk ticket delete --data '{"ids":["<id>"]}'        # POST /tickets/moveToTrash
zone desk recycle list --module-id <id>
zone desk recycle restore --data '{"ids":["<id>"]}'
zone desk recycle delete --data '{"ids":["<id>"]}'       # permanent
```

There is no hard DELETE for tickets, contacts, accounts, tasks, calls or events — they go to the recycle bin (`moveToTrash`) and are purged from there. Spam has its own path (`ticket mark-spam`, `delete-spam`, `empty-spam`).

#### Threads vs comments — the distinction that matters

A **thread** is the customer-facing conversation (the inbound email, your reply). A **comment** is a note on the ticket, internal or public. Replying to a customer is *not* a ticket update:

```bash
zone desk thread list <ticketId> --limit 20
zone desk thread latest <ticketId>                       # the most recent message
zone desk thread send-reply <ticketId> --data '{"channel":"EMAIL","to":"customer@acme.com","fromEmailAddress":"support@yourco.com","content":"<p>We are on it.</p>","contentType":"html"}'
zone desk thread draft <ticketId> --data '{…}'           # save without sending
zone desk thread split <ticketId> <threadId> --data '{"subject":"Split off"}'
zone desk ticket-comment add <ticketId> --data '{"content":"Waiting on the vendor","isPublic":false}'
```

`send-reply` sends mail — irreversible. `fromEmailAddress` must be a verified support address for that department (`zone desk supportemail list`). `isPublic: false` keeps a comment internal; `true` shows it in the Help Center.

#### Attachments, time, timers, followers, approvals

```bash
zone desk ticket-attach add <ticketId> --file ./log.txt          # multipart
zone desk ticket-attach list <ticketId>
zone desk ticket-time add <ticketId> --data '{"secondsSpent":"1800","chargeType":"Billable","agentId":"<id>"}'
zone desk ticket-time sum <ticketId>
zone desk ticket-timer set <ticketId> --action start             # start | pause | stop
zone desk ticket-follower add <ticketId> --data '{"followers":[{"id":"<agentId>","type":"AGENT"}]}'
zone desk ticket-approval create <ticketId> --data '{…}'
zone desk ticket-tag add <ticketId> --data '{"tags":[{"name":"vip"}]}'
```

#### Contacts, accounts and the rest of the modules

Tasks, calls, events, products, contracts and contacts all follow the same shape as tickets: `list` with `--from/--limit/--include`, `get`, `create`, PATCH `update`, `update-bulk` via `updateMany`, `delete` via `moveToTrash`, and a per-module `search`.

```bash
zone desk contact search --search "acme" --limit 20
zone desk account tickets <accountId> --limit 50
zone desk contact merge …   |   zone desk account merge <id> --data '{…}'
```

`zone desk search module <moduleApiName> --search "…"` reaches any module's search, including custom modules.

#### Knowledge base and community

KB articles and community posts live under a **Help Center**, and its id is part of the path rather than an injected header:

```bash
zone desk helpcenter list                                # helpCenterId
zone ctx desk helpCenterId=<id>                          # stored for your own reference; paths still take it
zone desk kbcat list --department-id <id>                # root categories
zone desk kbcat tree <rootCategoryId>
zone desk article list --category-id <id> --status Published --limit 50
zone desk article create --data '{"title":"Reset your password","categoryId":"<id>","answer":"<p>…</p>","permission":"ALL","status":"Published"}'
zone desk community category-list --department-id <id>
```

`article-tr` handles translations, `article-comment` / `article-feedback` the reader side, `kb-trash` deletions.

#### Reporting without exporting

```bash
zone desk dashboard created-tickets --group-by department --duration 30_days
zone desk dashboard solved-tickets  --group-by agent --duration 7_days --department-id <id>
zone desk dashboard response-time   --group-by team --is-first-response true --duration 30_days
zone desk happiness list --department <id>               # CSAT responses
zone desk ticket metrics <ticketId>                      # first response, resolution, reopen counts
```

`--group-by` and `--duration` are required on the dashboard endpoints. For bulk data use `export create` → `export get <exportId>` → `export download --export-id <id>`; imports are `import create` with `import mapping-create`.

#### Instant messaging, WhatsApp and drafts (0.8.3)

Generated from Zoho's `zohodesk-oas` repo for operations zone lacked — the notes on each row name the spec file and operationId, and they are not yet exercised against a live portal:

- **IM / WhatsApp** — the `im` group (`zone desk im channels`, then `--help` for the rest): IM channels, integration services, the WhatsApp phone-number OTP verification flow, sandbox testers, template tags/languages and template-credit status.
- **Drafts** — `ticket send-draft`, `ticket delete-draft-thread`, and `ticket resend-failed-send` for a thread whose send failed.
- **Rule groups** — `rule-group-execution-log` reads what a rule group did; `depmap` gains a count and a single get.
- **Not added:** the Solutions/channel attachment and photo uploads (multipart — use `zone api desk` with `--form`), and the `…Put` twins of operations zone already serves with PATCH.

#### Errors → what to do

| What you see | Meaning | Do |
|---|---|---|
| auth/permission error on any non-`/organizations` call | the `orgId` header is missing | `zone ctx desk orgId=<id>` (from `zone desk org list`) |
| "required parameter" on a settings read | a `--department-id` (or `--module`, `--view-id`) is missing | add it; the flag is marked `*` in this skill's command list |
| HTTP 204, empty output | no rows matched | not an error — stop paging |
| HTTP 422 with `errorCode` and a field name | validation: unknown status for that department, bad id, missing mandatory field | fix the named field; check `field list --module tickets --department-id <id>` |
| `INVALID_DATA` on `status` | that status does not exist in that department | list the department's statuses; write `status`, read `statusType` |
| 404 on a ticket you can see in the UI | it belongs to a department this agent cannot access, or another org | `department mine`, and confirm `orgId` |
| reply "from address not allowed" | `fromEmailAddress` is not a verified support address for the department | `zone desk supportemail list` |
| exit 3 | not signed in — Desk is its own consent | human runs `zone login desk` |
| exit 4 | token lacks the Desk scopes | human re-logs in that service |

Desk answers `200`/`204` on success with the record or nothing; a non-2xx carries `errorCode` and `message`, which zone surfaces as the last stderr line under `--json`.

> The `orgId` header, department scoping, offset paging, PATCH updates and the trash flow are all provable from the installed specs and zone's own service card. Response field names and the `cf` custom-field convention follow Zoho's Desk documentation but were **not** re-verified live, because this account has no Desk session — run `zone login desk` and they can be confirmed against a real org.
