New systems article covering the full VoIP stack: vendor model (PacketDial brand / NetSapiens platform / OIT wholesaler / YMCS phones), the PBX API + packetdial skill (reads + gated writes + onboard-domain), the Yealink/YMCS side + yealink-ymcs skill (one ACG key -> all client sites, RPS), the onboarding pipeline, vault paths, and known gotchas. Sources: the 3 06-22 session logs + both skill docs + the vendor-stack memory. Indexed under Systems. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
309 lines
16 KiB
Markdown
309 lines
16 KiB
Markdown
---
|
||
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 <key>` —
|
||
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 <METHOD> <path>` 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/<domain> --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/<domain> --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/<domain> --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 <vault-path> <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`).
|