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>
16 KiB
type, name, display_name, last_compiled, compiled_by, sources, backlinks
| type | name | display_name | last_compiled | compiled_by | sources | backlinks | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| system | packetdial | PacketDial (ACG VoIP / NetSapiens via OIT) | 2026-06-22 | GURU-5070/claude-main |
|
|
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
packetdialskill 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 theyealink-ymcsskill. 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:
POST /domains— creates the domain (Basic + Defaults + Limitations fields).POST /domains/{domain}/addresses/validate— validates the E911 address, returnsaddress-formatted-pidflo+emergency-address-id.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-typestores as"no"on create — the field sent as"Standard"came back"no"onvwp.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-addressleft 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 usesnotify@oitvoip.comas 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
alwaystype first, then convert. voip.packetdial.comhas no API — this is the customer-facing white-label portal (Cascades fax account 28598 lives here). Hitting/ns-api/*on this host returnserrors/not_found.- CDR queries are unbounded by default — always pass
--start/--endand--limit. - This is the live production PBX. Confirm the target domain with a read command before any write.
A bad
create-domainordelete-useraffects 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 |
| 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 intoymcs_client.py. - MSYS path conversion on
raw. A leading/v2/...argument gets rewritten by Git-Bash toC:/Program Files/Git/v2/.... Therawcommand auto-recovers (strips the prefix back to/v2/). Do NOT setMSYS_NO_PATHCONV=1— it breaks the vault subprocess and causes credential failures. Named wrappers (hardcoded paths) are unaffected. /v2/dm/sipAccountsis a CREATE endpoint, not a list. It is wrapped as the gatedadd-sipaccountwrite. 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 isadmin@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-typeif it stored as"no"(see PBX gotchas above). - Set
email-send-from-addressonce thevoicemail@packetdial.com/noreply@packetdial.commailbox 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-typestored as"no"on domain create (observed 2026-06-22). Re-PUT the field afteronboard-domain.onboard-domainwrapper does NOT auto-correct this — it is a platform behavior.- Voicemail user-defaults have no
POST /domainsfield. 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-addressmust 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 usesnotify@oitvoip.comas the working pattern.- Blocked-number filters inconclusive on empty domains.
block-numbersreturned 202 but the number did not persist on thearizonacomputergurutest 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, orrps-add/del. Exercise first on a non-critical device (e.g., a phone already due for reprovisioning). - Yealink known-bad firmware
96.86.0.20is a documented T54W brick-maker. Before any mass firmware push via YMCS, confirm the active firmware policy is NOT targeting this version. Seeclients/valleywide/docs/yealink-t54w-recovery-procedure.mdfor the TFTP recovery procedure. - OIT-API.txt plaintext was present at
C:\Users\guru\Downloads\OIT-API.txtas 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.serviceon the PBX, Syncro ticket #32375 (New Phone Install) open. - Skills:
.claude/skills/packetdial/(packetdial) and.claude/skills/yealink-ymcs/(yealink-ymcs).