---
name: yealink-ymcs
description: >
  Manage ACG's Yealink phone fleets via the Yealink Management Cloud Service (YMCS) v2 open API
  (us-api.ymcs.yealink.com). List sites/devices/accounts/firmwares/models/alarms, RPS servers +
  devices; gated writes (add devices by MAC, reboot/factory-reset, RPS add/remove, push a SIP
  account onto a device). Read-only by default; writes gated --confirm. Pairs with the `packetdial`
  skill (NetSapiens PBX) for phone provisioning. One ACG AccessKey sees ALL client sites.
  Triggers: yealink, ymcs, provision phone, register MAC, RPS, device firmware, reboot phone,
  push sip account to phone, yealink device manager.
---

# yealink-ymcs — Yealink Management Cloud Service (YMCS) v2 API

Manages the **physical Yealink phones** for ACG-managed clients. Pairs with **`packetdial`**:
the PBX (NetSapiens/PacketDial) owns the SIP accounts; **YMCS owns the phones** (provisioning,
config, firmware, RPS zero-touch). See [[reference_packetdial_oit_netsapiens]] for the vendor stack.

## Auth (v2 — token, not the legacy RPS HMAC)
`POST /v2/token` with HTTP **Basic** `base64(AccessKeyID:Secret)` (+ `timestamp` ms + `nonce`
headers + body `{"grant_type":"client_credentials"}`) → bearer token (~24h). Subsequent calls send
`Authorization: Bearer <token>`. Creds from vault **`services/yealink-ymcs.sops.yaml`**
(`credentials.access_key_id` / `access_key_secret`); env overrides `YMCS_ACCESS_KEY_ID` /
`YMCS_ACCESS_KEY_SECRET`; region `YMCS_REGION` (us|eu|au, default us → `us-api.ymcs.yealink.com`).
**Yealink servers require LEGACY TLS renegotiation** — handled in-client via an SSL context with
`OP_LEGACY_SERVER_CONNECT`.

## Account scope (verified 2026-06-22)
The ACG AccessKey is the **parent account** — one key sees **all client sites** as children:
`Arizona Computer Guru LLC` (parent) → `VWP` (Valley Wide Plastering), `GuruHQ`, `Ace Pick Up
Parks`. **No per-client keys needed.** (Per-client YMCS portal logins still exist, e.g. Valleywide
in `clients/valleywide/`, but the API key covers everything.)

## Commands
```bash
PY="$CLAUDETOOLS_ROOT/.claude/scripts/py.sh"; Y=".claude/skills/yealink-ymcs/scripts/ymcs.py"
# reads (live-verified [V] unless noted)
bash "$PY" "$Y" sites                 # [V] site tree (id/name/parentId/level)
bash "$PY" "$Y" devices [--site <id>] # [V] devices (keys: mac, modelName, deviceStatus, siteId, sn, programVersion, wanIp...) — --site filter is best-effort [P]
bash "$PY" "$Y" accounts              # [V] device accounts
bash "$PY" "$Y" device-groups | device-configs | firmwares | models | alarms | oplogs
bash "$PY" "$Y" official-firmwares    # [P] returns a non-standard shape; may need a model param
bash "$PY" "$Y" rps-servers           # [V] RPS provisioning servers (ACG: "WL - ACG" ftp://p.packetdials.net)
bash "$PY" "$Y" rps-devices           # [V] devices registered in RPS
# writes (ALL gated --confirm)
bash "$PY" "$Y" add-devices-by-mac --body '{...}' --confirm   # bulk-add phones by MAC to a site
bash "$PY" "$Y" add-sipaccount --body '{...}' --confirm        # push a SIP account onto a device (PBX glue)
bash "$PY" "$Y" reboot --body '{"ids":[...]}' --confirm        # reboot device(s)
bash "$PY" "$Y" reset  --body '{"ids":[...]}' --confirm        # FACTORY RESET — careful
bash "$PY" "$Y" rps-add --body '{...}' --confirm               # add device(s) to RPS (zero-touch)
bash "$PY" "$Y" rps-del --body '{...}' --confirm
bash "$PY" "$Y" raw POST /v2/dm/listDevices --body '{"skip":0,"limit":50,"autoCount":true}' [--confirm]
```
List endpoints page automatically (`{skip,limit,autoCount}` → `{total, data:[]}`); the wrappers
return `{total, count, data}` with all pages.

## How it pairs with packetdial (the onboarding pipeline)
1. **`packetdial onboard-domain`** → create the PBX domain + users + DIDs (NetSapiens SIP accounts).
2. **`yealink-ymcs`** → the phones: `add-devices-by-mac` into the client's site, `add-sipaccount`
   to push each NetSapiens SIP cred onto its phone, and/or `rps-add` so the phone zero-touch-
   provisions on first boot (RPS server `WL - ACG` = `ftp://p.packetdials.net`).
3. Result: new client → domain live → phones registered to `pbx.packetdial.com`.
**VWP is the live pilot** (just onboarded to the PBX; 21 devices in YMCS, fleet partly pending).

## Gotchas
- **Legacy TLS** (handled). If a future Python drops `OP_LEGACY_SERVER_CONNECT`, connections fail.
- **MSYS path conversion:** in `raw`, a leading `/v2/...` arg gets rewritten by Git-Bash to
  `C:/Program Files/Git/v2/...`. The `raw` command auto-recovers (cuts back to `/v2/`); don't use
  `MSYS_NO_PATHCONV=1` (it breaks the vault subprocess). Named wrappers are unaffected.
- `/v2/dm/sipAccounts` is the **create** endpoint (not a list) — wrapped as the gated
  `add-sipaccount`. There's no list-sip-accounts in the v2 API.
- Verification: token + sites/devices/accounts/rps-servers are [V]; writes + official-firmwares +
  the `--site` filter are [P] (plumbed per the n8n reference + spec, not lifecycle-exercised — no
  throwaway device to safely test writes on).

## Reference
- Vendor stack: [[reference_packetdial_oit_netsapiens]]. Portal login (web UI):
  `infrastructure/voip-phones.sops.yaml`. Per-client portal e.g. `clients/valleywide/`.
- Endpoint map harvested from the `dszp/n8n-nodes-yealinkymcs` community node + Yealink "Open API
  for YMCS V4X" doc. Full `/v2/dm/*` + `/v2/rps/*` surface reachable via `raw`.
