### Zoho Meeting — meetings and webinars are separate endpoints, `--index` is a row offset, and `count` means two different things

Four things cause most failures here: nothing works until `zsoid` is in ctx, a webinar key sent to a `meeting` command (or the reverse) fails as an HTTP **500** that zone reports as retryable, `--index` is a 1-based row number rather than a page, and the `count` in a response is the **total** while `--count` is the page size.

#### One context id, from the one call that does not need it

```bash
zone meetings org info                 # {"userDetails":{… "zsoid":700000001, "zuid":700000002 …}}
zone ctx meetings zsoid=<zsoid>        # fills every :zsoid path segment
```

Without it every path-bearing command stops before the request goes out:

```
BAD_INPUT: Missing <zsoid> for this command.
```

`org info` and the three `misc` commands are the only ones that carry no `:zsoid`. The record also holds `zuid` — your **user** id, which is what `presenter` and `coHosts[].zuid` are, and is not interchangeable with `zsoid`.

#### Meetings and webinars are different objects with different keys

```bash
zone meetings meeting list --type all --count 50 --index 1
zone meetings webinar list --type all --count 50
```

Both answer under the same envelope key — `{"session":[…], "count":<total>}` — which is the reason they get confused. They are not the same collection: on this org `meeting list` returns 3,179 rows and none of them is one of the 8 webinars.

| | meetings | webinars |
|---|---|---|
| id called | `meetingKey` | `webinarKey` (also printed as `meetingKey` in the list) |
| commands | `meeting list/get/create/update/delete/participants` | `webinar list/get/create/update/delete/register/attendees/registrations`, plus all of `poll` and `report` |
| who attended | `meeting participants` | `webinar attendees`, `report attendees` |

`webinar get <a meeting key>` returns **HTTP 500**, which zone classifies as `RETRYABLE` — an agent that retries on that code will loop against a request that can never succeed. The asymmetry is worth knowing: `meeting get` *does* accept a webinar key and answers with `sessionType:"webinar"`, so a successful `meeting get` is not proof you are holding a meeting.

`--type` (`-t`) accepts `all`, `past` and `upcoming`; the default is `all` with 20 rows. **`--type today` returns HTTP 500** despite being a documented value, and anything else is rejected with `400 PATTERN_NOT_MATCHED`.

#### Paging: `--index` overlaps, and no cap was found

Verified against the live org with `--count 2`:

| call | rows returned |
|---|---|
| `--index 0` | rows 1, 2 |
| `--index 1` | rows **1**, 2 |
| `--index 2` | rows **2**, 3 |
| `--index 3` | rows **3**, 4 |

So `--index` is a 1-based row number: advance it by `--count`, not by one. `--count 5000` returned all 3,179 rows in a single response, so there is no page cap to work around — but the `count` field in the envelope is the **total matching rows**, not how many came back. Reading it as a page size is how loops end early.

#### Several endpoints require paging flags that look optional

```bash
zone meetings webinar attendees <webinarKey> --index 1 --count 50
zone meetings meeting participants <meetingKey> --index 1 --count 50
zone meetings webinar registrations <webinarKey> --status 1
```

Omitting one gives `HTTP 400: LESS_THAN_MIN_OCCURANCE`, and the `zoho.error.parameter_name` in the JSON error names which one is missing. `webinar attendees` and `meeting participants` need **both** `--index` and `--count`. `webinar registrations` instead requires `--status`: `1` approved, `0` pending, `2` denied, `3` cancelled — asking for one status at a time, with the totals for all of them in `meta` (`totalCount`, `approvedCount`, `pendingCount`, `deniedCount`, `isAutoApprovalEnabled`). Every `poll` and `report` command requires `--webinar-key`, which zone catches locally as `BAD_INPUT` before the request.

Envelope keys differ per family and none of them is `data`: `session` for meetings and webinars, `recordings` for recordings, `registrants` for registrations, `attendeeData` for `webinar attendees`, `polls`/`pollData`/`attendees`/`questions` for polls and reports, and `representation` for everything under `org` and `misc`.

Note the two attendee reads are not redundant: `webinar attendees` gives a thin `{name, email, questions[], polls[]}`, while `report attendees` gives the engagement record — `joinedTimeInMillis`, `leftTimeInMillis`, `duration`, `registerKey`, `approvalStatus`, poll and question counts.

#### Times, durations and timezones

```bash
zone meetings misc timezones          # the allowed vocabulary: {timeZone, offset, timeZoneFullName}
```

- `timezone` is an IANA name (`Africa/Cairo`, `Asia/Dubai`), taken from `misc timezones`.
- `startTime` on a read is a **rendered string in the session's own timezone**, abbreviation included: `"Sep 05, 2026 12:15 PM EEST"`. A `meeting get` also returns `localTime`, the same instant in *your* profile timezone, with a `noteText` spelling out the difference. `startTimeMillisec` (epoch ms) is the only unambiguous field — compare and sort on that, never on the string.
- On a write, `startTime` goes in without the abbreviation: `"Jun 19, 2026 07:00 PM"`, interpreted in the `timezone` you send alongside it.
- `duration` is **milliseconds** (`3600000` = one hour) and comes back as a number in a list and a string in a `get`.
- `presenter` is a `zuid` string, from `zone meetings org users`.

`meeting create` and `meeting update` both wrap the payload as `{"session":{…}}` (`{"webinar":{…}}` for webinars). `update` is a **PUT** and zone sends exactly what you hand it, so read the meeting first and resend the fields you mean to keep rather than a single changed key.

#### Recordings: two different ids, and a list you cannot page

```bash
zone meetings recording list                     # 20 rows, meta.moreRecords tells you there are more
zone meetings recording get <meetingKey>
zone meetings recording transcript <meetingKey> -o ./transcript.txt
zone meetings recording data <meetingKey> --no-summary
```

A recording row carries `meetingKey`, `id` (the session's sysId) **and** `erecordingId`, a 64-character hex string. The read commands take the `meetingKey`; `recording delete` takes the `erecordingId` — mixing them is how the wrong thing gets deleted.

`recording list` has no paging flags at all, so it returns the first 20 while `meta` reports `{"count":2732,"moreRecords":true}`. Page it through the proxy, with the same 1-based index:

```bash
zone api meetings GET "/meeting/api/v2/<zsoid>/recordings.json?index=21&count=20"
```

`recording summary` exits with `No summary available for this recording` when none was generated — check `isSummaryGenerated` / `isTranscriptGenerated` on the row first. Video, transcript and summary come from the `download.zoho.com` gateway rather than the REST base, so they need the file scopes as well.

#### What does not exist, and what you cannot undo

There is no start-meeting, end-meeting or registrant-approve/deny/cancel API — Zoho never shipped them, so those steps stay manual. The whole `sdk` group needs the AV SDK enabled on the org; without it every call answers `HTTP 400: AVSDK is not enabled` (code 7002).

`meeting delete`, `webinar delete`, `recording delete` and `poll delete` are permanent — there is no trash and no restore. `webinar register` mails the registrant a join link that cannot be recalled, and with no cancellation API the registration cannot be withdrawn through the API either.

#### Errors → what to do

| What you see | Meaning | Do |
|---|---|---|
| `BAD_INPUT: Missing <zsoid>` | ctx has no org id | `zone meetings org info`, then `zone ctx meetings zsoid=<id>` |
| `HTTP 500` / `RETRYABLE` on a `webinar` command | a meeting key was sent to a webinar endpoint | use the `meeting` group, and do not retry |
| `HTTP 500` on `meeting list` | `--type today` | use `all`, `past` or `upcoming` |
| `400 PATTERN_NOT_MATCHED` | an unrecognised `--type` value | one of `all`, `past`, `upcoming` |
| `400 LESS_THAN_MIN_OCCURANCE` | a required paging/filter flag is missing | read `zoho.error.parameter_name`; add `--index`+`--count`, or `--status` |
| `Missing required option: --webinar-key` | every `poll` and `report` call is webinar-scoped | pass `--webinar-key <key>` |
| duplicate rows while paging | `--index` treated as a page number | advance `--index` by `--count` |
| a loop stopping early | `count` in the response is the total, not the page size | keep paging until you have `count` rows |
| only 20 recordings ever | `recording list` exposes no paging | page via `zone api …/recordings.json?index=N&count=20` |
| `400 AVSDK is not enabled` | the SDK add-on is off for this org | not fixable from the API |
| exit 3 | not signed in — Meeting is its own consent | human runs `zone login meetings` |
| exit 4 | token lacks a `ZohoMeeting.*` scope | human re-logs in that service |

> Command names, paths, required flags and HTTP verbs come from the installed specs, so they are exact. Everything else here was **verified live against a real Zoho Meeting org** while writing this: the `zsoid` bootstrap, the shared `session` envelope for both meetings and webinars and the fact that the two collections do not overlap, the 500 from `webinar get` on a meeting key and the 500 from `--type today`, the overlapping 1-based `--index`, `count`-as-total, the absence of a page cap, the `LESS_THAN_MIN_OCCURANCE` requirements on `webinar attendees` / `meeting participants` / `webinar registrations`, every envelope key listed above, the `erecordingId` vs `meetingKey` split, the unpageable `recording list` with `meta.moreRecords`, the `startTime` / `localTime` / `startTimeMillisec` timezone behaviour, and the `AVSDK is not enabled` response. Only read commands were run — nothing was created, started, sent or deleted, so the `{"session":{…}}` write shape and the write-side `startTime` format follow zone's service card and Zoho's API guide.
