--- type: system name: packetdial display_name: "PacketDial (ACG VoIP / NetSapiens via OIT)" last_compiled: 2026-06-22 compiled_by: GURU-5070/claude-main sources: - .claude/memory/reference_packetdial_oit_netsapiens.md - .claude/skills/packetdial/SKILL.md - .claude/skills/packetdial/references/api.md - .claude/skills/yealink-ymcs/SKILL.md - session-logs/2026-06/2026-06-22-mike-packetdial-buildout-oitvoip-vault.md - session-logs/2026-06/2026-06-22-mike-packetdial-domain-onboarding-vwp.md - session-logs/2026-06/2026-06-22-mike-yealink-ymcs-skill.md - wiki/clients/valleywide.md backlinks: - clients/valleywide --- # PacketDial (ACG VoIP / NetSapiens via OIT) > Two things live under this article: (1) the **PacketDial/NetSapiens PBX platform** — ACG's > hosted VoIP offering, managed via the `packetdial` skill against the NetSapiens API v2; and > (2) the **Yealink YMCS phone-management layer** — the cloud device manager that handles > physical phone provisioning, firmware, and RPS zero-touch, managed via the `yealink-ymcs` > skill. The two skills form the end-to-end onboarding pipeline for new VoIP clients. --- ## Vendor Stack Do not conflate these four layers: | Layer | What it is | Hostname / Identifier | |---|---|---| | **PacketDial** | ACG's own VoIP department / customer-facing brand | `pbx.packetdial.com` (API host); `voip.packetdial.com` (customer portal — no API) | | **NetSapiens** | The underlying PBX/UCaaS software platform (SNAPsolution API v2, v44.4.10) | Same hosts as above | | **OIT / OITVOIP** | White-label wholesale provider that runs NetSapiens and resells to MSPs; ACG's upstream | `api.ucaasnetwork.com` (OIT/NetSapiens white-label host — same platform as `pbx.packetdial.com`) | | **YMCS (Yealink Management Cloud Service)** | Separate Yealink cloud for physical phone device management | `us-api.ymcs.yealink.com` (US region) | `pbx.packetdial.com` and `api.ucaasnetwork.com` resolve to the same NetSapiens platform under different white-label hostnames. The `packetdial` skill targets `pbx.` by default; if a future endpoint 403s there, override with `PACKETDIAL_API_BASE_URL=https://api.ucaasnetwork.com/ns-api/v2`. ACG's reseller territory on OIT's platform is `91912.service`. --- ## PBX Platform (NetSapiens via PacketDial / OIT) ### API | Field | Value | |---|---| | Base URL | `https://pbx.packetdial.com/ns-api/v2` | | Token endpoint | `https://pbx.packetdial.com/ns-api/v2/tokens` | | Spec version | NetSapiens API v2 v44.4.10 — 239 paths / 354 operations (GET 139, POST 87, PUT 68, DELETE 50, PATCH 10) | | OpenAPI spec | `https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json` | | Swagger UI | `https://pbx.packetdial.com/ns-api/openapi` | | Scope | Almost everything nested under `/domains/{domain}/...` | **Auth — static API key (in use):** Send the `nsr_` key directly as `Authorization: Bearer ` — no exchange step. Key prefix encodes scope: `nsr_` = reseller, `nss_` = system, `nsd_` = domain. OAuth2 password grant (`POST /tokens` form-encoded) is also supported; the client caches and refreshes JWTs ~60s before expiry. **Reseller key (live-verified 2026-06-22):** | Field | Value | |---|---| | Key-ID | `nsr_hSGUB5Wo` | | Scope | Reseller `91912.service`; `user/domain: *` (sees every domain under ACG's territory) | | Permissions | Read-write (`readonly: no`); cannot create sub-keys (`can-create-keys: no`) | | Vault path | `msp-tools/oitvoip.sops.yaml` → `credentials.api_key` | ### Reseller Domains (as of 2026-06-22) | Domain | Purpose | Notes | |---|---|---| | `0000.91912.service` | Reseller default / template | OIT platform default | | `arizonacomputerguru` | ACG's own domain | **Sanctioned test bed — not in production use.** Use for exercising write wrappers safely. | | `russo.91912.service` | Client domain | | | `vwp.91912.service` | Valley Wide Plastering | Created 2026-06-22 via `onboard-domain`. E911 address id `a-6a395c03d4cfe` (301 N 56TH ST, Chandler AZ 85226, USPS VALID). Caller-ID 4807059500. | ### `packetdial` Skill Skill location: `.claude/skills/packetdial/` (scripts: `ns.py`, `ns_client.py`). **Reads (read-only by default, no gate):** `domains`, `domain`, `users`, `user`, `phones`, `dids`, `devices`, `callqueues`, `timeframes`, `sites`, `autoattendants`, `contacts`, `billing`, `addresses`, `smsnumbers`, `blocked-numbers`, `moh`, `dialrules`, `recording`, `transcriptions`, `cdrs`, `resellers` All listed reads are live-verified against the production reseller key (2026-06-22). **Writes (every mutating command is `--confirm`-gated; prints `[DRY RUN]` and exits non-zero without the flag):** | Resource | Wrappers | Verification | |---|---|---| | Domains | `create-domain`, `onboard-domain` | `onboard-domain` end-to-end verified (vwp.91912.service) | | Users | `create-user`, `update-user`, `delete-user` | [P] | | Call queues | `create/update/delete-callqueue`, `add/update/remove-agent` | [V] full lifecycle on `arizonacomputerguru` | | Time frames | `create/update/delete-timeframe` | [V] full lifecycle on `arizonacomputerguru` | | Contacts | `create/update/delete-contact` | [V] on `arizonacomputerguru` | | DIDs | `create-did`, `update-did`, `delete-did` | [P] | | SIP devices | `create-phone`, `create/update/delete-device` | [P] | | E911 addresses | `create/update/delete-address` | [P] (create requires `addresses/validate` → pidflo first) | | Sites | `create-site`, `update-site` | [P] | | Auto-attendants | `create-autoattendant` | [P] | | SMS numbers | `create/update/delete-smsnumber` | [P] (requires SMS-provisioned domain) | | Blocked numbers | `block-numbers`, `unblock-numbers` | [P] (202'd but didn't persist on empty test domain) | | Music on hold | `create-moh` (TTS), `delete-moh` | [P] (multipart file upload → use `raw`) | **[V]** = lifecycle-verified on ACG test domain; **[P]** = plumbed per spec, not lifecycle-verified. **Raw escape hatch:** `ns.py raw ` reaches any of the 239 v2 paths. Non-GET methods still require `--confirm`. ### `onboard-domain` Wrapper One gated command that runs the OITVOIP "Add a Domain" GUI wizard as 3 API calls: 1. `POST /domains` — creates the domain (Basic + Defaults + Limitations fields). 2. `POST /domains/{domain}/addresses/validate` — validates the E911 address, returns `address-formatted-pidflo` + `emergency-address-id`. 3. `POST /domains/{domain}/addresses` — creates the E911 address with the pidflo from step 2. Usage: `ns.py onboard-domain --body-file new-client.json --confirm` The body is the `POST /domains` schema plus an optional `emergency` sub-object carrying the E911 address fields. Omit `limits-max-*` for unlimited seat counts (they default to 0 = uncapped; do not set them to a sentinel). Omit `dial-plan` (auto-generates named after the domain). Undo a bad onboard: `ns.py raw DELETE domains/ --confirm`. ### PBX Gotchas - **`domain-type` stores as `"no"` on create** — the field sent as `"Standard"` came back `"no"` on `vwp.91912.service`. Fix post-create: `ns.py raw PUT domains/ --body '{"domain-type":"Standard"}' --confirm`. - **Voicemail user-defaults not in `POST /domains`** — the GUI Defaults tab (Enable VM, Transcription, Message) has no equivalent in the domain-create schema; how the GUI applies them is unresolved (verify). - **`email-send-from-address` left blank on create** — must be set separately once the sender mailbox exists: `ns.py raw PUT domains/ --body '{"email-send-from-address":"voicemail@packetdial.com"}' --confirm`. ACG's own domain uses `notify@oitvoip.com` as the working pattern. - **Unlimited seat counts = omit `limits-max-*`** — do not pass 0 or a sentinel; omitting the fields lets the platform default to uncapped. - **Timeframe writes are body-discriminated** — the same path handles all operations; the server selects create/update/delete based on the request body. Entry-bearing types (days-of-week, etc.) reject creation without their array — create as `always` type first, then convert. - **`voip.packetdial.com` has no API** — this is the customer-facing white-label portal (Cascades fax account 28598 lives here). Hitting `/ns-api/*` on this host returns `errors/not_found`. - **CDR queries are unbounded by default** — always pass `--start`/`--end` and `--limit`. - **This is the live production PBX.** Confirm the target domain with a read command before any write. A bad `create-domain` or `delete-user` affects real customers. --- ## Phone Management (Yealink YMCS) ### API | Field | Value | |---|---| | Base URL | `https://us-api.ymcs.yealink.com` (US region; EU = `eu-api.ymcs.yealink.com`; AU = `au-api.ymcs.yealink.com`) | | Region env | `YMCS_REGION` (us | eu | au; default us) | | Auth | `POST /v2/token` with HTTP Basic `base64(AccessKeyID:Secret)` + headers `timestamp` (ms) + `nonce` + body `{"grant_type":"client_credentials"}` → bearer token (~24h) | | TLS | **Requires legacy TLS renegotiation** — handled in-client via `OP_LEGACY_SERVER_CONNECT` SSL context | | Vault path | `services/yealink-ymcs.sops.yaml` → `credentials.access_key_id` / `credentials.access_key_secret` | ### Account Tree (verified 2026-06-22) The ACG AccessKey is the **parent account** — one key covers all client sites as children. No per-client API keys are needed. ``` Arizona Computer Guru LLC (parent / ACG account) ├── VWP (Valley Wide Plastering) ├── GuruHQ (ACG office) └── Ace Pick Up Parks ``` ### RPS Provisioning Server | Field | Value | |---|---| | Server name | `WL - ACG` | | URL | `ftp://p.packetdials.net` | | Purpose | Zero-touch provisioning — phones pull their config from this RPS server on first boot | ### `yealink-ymcs` Skill Skill location: `.claude/skills/yealink-ymcs/` (scripts: `ymcs.py`, `ymcs_client.py`). **Reads (live-verified unless noted):** `sites`, `devices` (21 total; `--site` filter best-effort [P]), `accounts`, `rps-servers`, `rps-devices`, `device-groups`, `device-configs`, `firmwares`, `models`, `alarms`, `oplogs` `official-firmwares` returns a non-standard shape (likely requires a model param) — marked [P]. List endpoints auto-paginate (`{skip,limit,autoCount}` → `{total,data:[]}`); wrappers return `{total, count, data}` across all pages. **Writes (all `--confirm`-gated; not lifecycle-verified — no throwaway device to safely test against):** | Command | Effect | |---|---| | `add-devices-by-mac` | Bulk-add phones by MAC address to a site | | `add-sipaccount` | Push a SIP credential onto a device (the PBX↔phone integration glue) | | `reboot` | Reboot device(s) by ID | | `reset` | Factory-reset device(s) — destructive | | `rps-add` | Register device(s) in RPS for zero-touch provisioning | | `rps-del` | Remove device(s) from RPS | **Raw passthrough:** `ymcs.py raw POST /v2/dm/listDevices --body '{...}'` reaches any `/v2/dm/*` or `/v2/rps/*` endpoint. ### YMCS Gotchas - **Legacy TLS required.** If a future Python update drops `OP_LEGACY_SERVER_CONNECT`, connections to YMCS will silently fail. The SSL context with that flag is baked into `ymcs_client.py`. - **MSYS path conversion on `raw`.** A leading `/v2/...` argument gets rewritten by Git-Bash to `C:/Program Files/Git/v2/...`. The `raw` command auto-recovers (strips the prefix back to `/v2/`). Do NOT set `MSYS_NO_PATHCONV=1` — it breaks the vault subprocess and causes credential failures. Named wrappers (hardcoded paths) are unaffected. - **`/v2/dm/sipAccounts` is a CREATE endpoint, not a list.** It is wrapped as the gated `add-sipaccount` write. There is no list-SIP-accounts endpoint in the v2 API. - **Per-client portal logins exist** (e.g., vault `clients/valleywide/`) even though the API key covers all clients. The portal admin for the ACG parent account is `admin@azcomputerguru.com`. --- ## Onboarding Pipeline New VoIP client flow (the two skills together): ``` 1. packetdial onboard-domain → PBX domain created + E911 address validated and registered. 2. packetdial create-user → SIP extensions created per user. 3. packetdial create-did → DIDs attached and routed to users. 4. yealink-ymcs add-devices-by-mac → Physical phones added to the client's YMCS site. 5. yealink-ymcs add-sipaccount → NetSapiens SIP credential pushed onto each phone. 6. yealink-ymcs rps-add → (Optional) Phone registered in RPS WL-ACG for zero-touch on first boot (pulls config from ftp://p.packetdials.net). ``` Result: new client → domain live → phones registered to `pbx.packetdial.com`. **VWP is the live pilot** (2026-06-22): `vwp.91912.service` on the PBX, 21 devices in YMCS (fleet partly pending — 16x Yealink T54W plus others). Steps 4–6 not yet exercised on production VWP hardware (writes are [P] for YMCS). **Post-onboard manual steps (gaps vs. full GUI wizard):** - Fix `domain-type` if it stored as `"no"` (see PBX gotchas above). - Set `email-send-from-address` once the `voicemail@packetdial.com` / `noreply@packetdial.com` mailbox exists (pending infra task as of 2026-06-22). - Apply voicemail user-defaults — mechanism for applying the GUI Defaults tab (Enable VM, Transcription, Message) via the API is unresolved (verify). --- ## Access | Credential | Vault Path | Field(s) | |---|---|---| | NetSapiens reseller API key | `msp-tools/oitvoip.sops.yaml` | `credentials.api_key` | | YMCS API AccessKey | `services/yealink-ymcs.sops.yaml` | `credentials.access_key_id`, `credentials.access_key_secret` | | Yealink portal admin (`admin@azcomputerguru.com`) | `infrastructure/voip-phones.sops.yaml` | (verify field names) | | VWP per-client YMCS portal | `clients/valleywide/` | (verify entry name) | Read a field: `bash .claude/scripts/vault.sh get-field ` **NEVER inline raw secrets or API key values** — reference vault paths only. --- ## Known Issues & Quirks - **`domain-type` stored as `"no"` on domain create (observed 2026-06-22).** Re-PUT the field after `onboard-domain`. `onboard-domain` wrapper does NOT auto-correct this — it is a platform behavior. - **Voicemail user-defaults have no `POST /domains` field.** The GUI Defaults tab (Enable Voicemail, Transcription, Message to Email) is not represented in the domain-create schema. How and where the GUI applies these defaults is unresolved; set manually post-onboard (verify the mechanism). - **`email-send-from-address` must be set separately.** Required for voicemail-to-email delivery. The sender mailboxes (`voicemail@packetdial.com` / `noreply@packetdial.com`) were not yet created as of 2026-06-22. ACG's own domain uses `notify@oitvoip.com` as the working pattern. - **Blocked-number filters inconclusive on empty domains.** `block-numbers` returned 202 but the number did not persist on the `arizonacomputerguru` test domain (empty). Verify on a live domain with real traffic before relying on this feature. - **YMCS write wrappers are [P] (plumbed, not lifecycle-verified).** No throwaway device was available to safely test `add-devices-by-mac`, `add-sipaccount`, `reboot`, `reset`, or `rps-add/del`. Exercise first on a non-critical device (e.g., a phone already due for reprovisioning). - **Yealink known-bad firmware `96.86.0.20`** is a documented T54W brick-maker. Before any mass firmware push via YMCS, confirm the active firmware policy is NOT targeting this version. See `clients/valleywide/docs/yealink-t54w-recovery-procedure.md` for the TFTP recovery procedure. - **OIT-API.txt plaintext** was present at `C:\Users\guru\Downloads\OIT-API.txt` as of 2026-06-22. Mike to delete; key is in the vault. --- ## Backlinks - [[clients/valleywide]] — VWP is the live pilot client; 21 Yealink devices in YMCS, `vwp.91912.service` on the PBX, Syncro ticket #32375 (New Phone Install) open. - Skills: `.claude/skills/packetdial/` (`packetdial`) and `.claude/skills/yealink-ymcs/` (`yealink-ymcs`).