### Zoho Projects — V3, portal-in-the-path, and a vocabulary that differs from the UI

Four things cause most failures here: this is the **V3** API (the old `/restapi` was switched off on 2025-12-31), the portal id is a **path segment** rather than a header or query param, the API's nouns are **not** the UI's nouns, and paging uses yet another shape (`page_info.has_next_page`).

#### The portal id lives in the path

```bash
zone api projects GET /portals            # or: zone projects portal list
zone ctx projects portalId=<id>           # substituted into every :portalId path segment
zone projects project list --toon
```

Every path is `/portal/<portalId>/…`. Store the id once and zone fills the segment; without it the path is malformed rather than unauthorized, so the error looks like a bad URL. `portal list` is the only call that does not need it.

#### The nouns are not the ones in the UI

| In the UI | In the API and in `zone` | Commands |
|---|---|---|
| Bug / defect | **issue** | `zone projects issue list`, `issue create <projectId>` |
| Milestone | **phase** | `zone projects phase list`, `phase create <projectId>` |
| Task list | tasklist | `zone projects tasklist list <projectId>` |
| Custom module record | **entity** under `record` | `zone projects record list <moduleId>` |
| Client / customer portal user | **client** | `zone projects client list` |

Searching the command list for `bug` or `milestone` finds nothing. Use `issue` and `phase`.

#### Hierarchy, and the two shapes of every list

Portal → project → (phase, tasklist → task, issue). Most collections exist twice: **portal-wide** and **per project**.

```bash
zone projects task all --per-page 100            # every task in the portal
zone projects task list <projectId>              # tasks in one project
zone projects issue list                         # portal-wide issues
zone projects issue project-list <projectId>     # issues in one project
zone projects phase list                         # portal-wide phases
zone projects tasklist all
zone projects timelog all --view-type week
```

The portal-wide form is how you answer "everything assigned to X" without looping over projects. Note the naming is not uniform: tasks use `all` / `list <projectId>`, while issues and phases use `list` / `project-list <projectId>`.

#### Paging — a third style again

```bash
zone projects task all --page 1 --per-page 100 --toon
```

Responses carry `page_info` with **`has_next_page`**; keep incrementing `--page` while it is true. This is not CRM's `more_records`, not Books' `page_context.has_more_page`, and not Desk's `from`/`limit`.

**Rate limit: 200 requests per 2 minutes, per endpoint.** Loops over projects hit this quickly — prefer the portal-wide list with a filter over N per-project calls, and back off when you see a throttle.

#### Creating and updating

```bash
zone projects project create --data '{"name":"Website rebuild","description":"…","start_date":"2026-09-01","end_date":"2026-12-15","owner_id":"<userId>"}'
zone projects project update <projectId> --data '{"status":"active"}'

zone projects tasklist create <projectId> --data '{"name":"Phase 1 — discovery"}'
zone projects task create <projectId> --data '{"name":"Draft the sitemap","tasklist_id":"<id>","owner_ids":["<userId>"],"start_date":"2026-09-02","end_date":"2026-09-06","priority":"High"}'
zone projects task update <projectId> <taskId> --data '{"percent_complete":"50"}'
zone projects issue create <projectId> --data '{"title":"Checkout 500s on Safari","description":"…","severity_id":"<id>","assignee_id":"<userId>"}'
zone projects phase create <projectId> --data '{"name":"Launch","start_date":"2026-11-01","end_date":"2026-12-15"}'
```

- A write body **is the record** — no `{"data":[…]}` envelope, one call per record.
- Updates are **PATCH** and partial: send only what changes. One exception worth knowing: **`phase update` is a POST**, not a PATCH, while `phase move` is a PATCH — the reverse of everything around it.
- Dates are `yyyy-MM-dd`. Ids are numeric **strings**; keep them as strings.
- Custom fields are defined per module: `zone projects field list --module tasks`, and layouts via `zone projects layout list --module tasks`.

#### Time logs

```bash
zone projects timelog add <projectId> --data '{"task_id":"<id>","date":"2026-09-02","hours":"2","minutes":"30","bill_status":"Billable","notes":"Sitemap review"}'
zone projects timelog project-list <projectId> --view-type week --start-date 2026-09-01
zone projects timelog bulk-add --data '{…}'
zone projects timelog report --report-type user --view-type month
zone projects timelog approvers <projectId>
```

`--view-type` is required on the portal-wide `timelog all`, and `--report-type` on the report endpoints. Time logs attach to a task or an issue, not to a project directly.

#### Custom modules

```bash
zone projects module list                                   # module ids
zone projects record list <moduleId> --page 1 --per-page 50
zone projects record create <moduleId> --data '{…}'
zone projects record trash <moduleId> <entityId> --data '{}'
zone projects record restore <moduleId> <entityId>
```

Records in custom modules are **entities**, reached through the module id rather than a named path.

#### Deleting: trash first, bin second

```bash
zone projects task delete <projectId> <taskId>       # DELETE — gone
zone projects project trash <projectId> --data '{}'  # to the bin
zone projects portal bin --page 1                    # what is in the bin
zone projects portal bin-restore --data '{"ids":["<id>"]}'
zone projects portal bin-delete  --data '{"ids":["<id>"]}'
zone projects portal bin-empty                       # permanent, everything
```

Projects and custom-module entities go to a recoverable **bin**; tasks, issues and tasklists have a direct `delete`. Check which you are calling before assuming it is reversible.

#### People and access

```bash
zone projects user list --type active
zone projects project users <projectId>
zone projects project user-add <projectId> --data '{"email":"dev@acme.com","role":"employee"}'
zone projects client list                            # external client companies
zone projects client adduser <clientId> --data '{…}'
zone projects permission profiles <category> --profile-ids <ids>
zone projects permission grant <profileId> <category> --data '{…}'
```

Portal users are staff; **clients** are the external side with their own users and a restricted view.

#### Attachments

```bash
zone projects attachment list <projectId> --entity-type task --entity-id <taskId>
zone projects attachment upload --file ./spec.pdf
zone projects attachment associate --data '{…}'
```

`--entity-type` and `--entity-id` are **required** on list and dissociate: attachments belong to a task or issue, not to the project as a whole. Uploading and associating are two steps.

#### License, bulk tasks, subtasks, custom-module state (0.8.3)

Found by extracting all 483 endpoints from the Projects API docs page and diffing:

```bash
zone projects portal license                          # plan, owner, company — verified live
zone projects task bulk-create <projectId> --data '{...}'   # the portal-level bulk update stays `task bulk-update`
zone projects task make-as-subtask <projectId> <taskId> --data '{...}'
zone projects task make-as-task <projectId> <taskId>  # promote a subtask
zone projects module activate <moduleId>              # and deactivate
```

- The task/issue timelog **timers** are already covered: `timelog timer-get` and `timer-notes` take the module (`task` or `issue`) as `:moduleName`.
- The docs show a v3.1 `PATCH` for phase updates; zone's v3 `phase update` (POST) does the same job.

#### Errors → what to do

| What you see | Meaning | Do |
|---|---|---|
| a malformed-URL or 404 on every call | `portalId` is missing from the path | `zone ctx projects portalId=<id>` (from `zone projects portal list`) |
| "no such command" for `bug` or `milestone` | wrong vocabulary | use `issue` and `phase` |
| 404 on `/restapi/...` | the V2 API was retired on 2025-12-31 | use the V3 commands; zone is already on V3 |
| "required parameter" on a settings or report read | `--module`, `--view-type`, `--report-type` or `--entity-type` is missing | the required flags are marked `*` in this skill's command list |
| throttled after a burst | 200 requests per 2 minutes per endpoint | use the portal-wide list with a filter instead of looping projects; back off |
| a task or issue id that 404s | ids are scoped to their project — the project id in the path must match | fetch it from that project's list |
| exit 3 | not signed in — Projects is its own consent | human runs `zone login projects` |
| exit 4 | token lacks the Projects scopes | human re-logs in that service |

> Command names, paths, required flags, HTTP verbs (including the `phase update` POST) and the V3 base URL come from the installed specs and zone's service card, so they are exact. Response field names in the example bodies follow Zoho's V3 documentation but were **not** re-verified live — this account has no Projects session. Run `zone login projects` and they can be confirmed against a real portal.
