Added write wrappers, each tested create→update→delete on the arizonacomputerguru test domain (sanctioned, non-production): - call queues: create-callqueue, update-callqueue, delete-callqueue + add-agent / update-agent / remove-agent - time frames: create-timeframe, update-timeframe, delete-timeframe (body-discriminated — same path, server selects the op from the body; wrappers pass --body verbatim) All behind --confirm (gate verified: DRY RUN refuses without it). SKILL.md documents the bodies + the days-of-week-needs-array gotcha + names ACG as the test bed; capability map + api.md history updated. No production objects touched; no test leftovers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
name, description
| name | description |
|---|---|
| packetdial | Manage the ACG PacketDial/OITVOIP hosted VoIP via the NetSapiens API v2 (pbx.packetdial.com). List/inspect domains, users, devices, DIDs, call queues, time frames, sites, auto-attendants, contacts, and domain billing/limits; pull CDRs; provision domains/users/SIP/numbers (writes gated --confirm; read-only default). Reseller-scoped key live + vaulted. Triggers: packetdial, oitvoip, netsapiens, voip domain/user/extension, call queue, auto attendant, provision phone, add did, CDR. Live production PBX. |
PacketDial / NetSapiens (OITVOIP) Skill
Standalone CLI client for the NetSapiens SNAPsolution API v2 that backs
ACG's hosted-VoIP offering through OITVOIP / PacketDial. Read-only by default;
every write (create / update / delete) is gated behind --confirm.
The two hostnames (important)
| Host | What it is | API? |
|---|---|---|
voip.packetdial.com |
Customer-facing white-label portal / UC & fax dashboard (e.g. Cascades fax account 28598). Login-gated UI. | No |
pbx.packetdial.com |
Reseller PBX platform — NetSapiens v44.4. | Yes — this skill targets it |
- API base:
https://pbx.packetdial.com/ns-api/v2 - Token endpoint:
https://pbx.packetdial.com/ns-api/v2/tokens - Live OpenAPI spec:
https://pbx.packetdial.com/ns-api/webroot/openapi/openapi.json - Live Swagger UI:
https://pbx.packetdial.com/ns-api/openapi - Vendor docs: https://docs.ns-api.com/ (login) and https://voipdocs.io/oitvoip-access-platform-apis
Credentials — PROVISIONED + LIVE-VERIFIED (2026-06-22)
The reseller API key is vaulted at msp-tools/oitvoip.sops.yaml →
credentials.api_key (static nsr_ bearer key). Live-verified via ns.py whoami:
key-id nsr_hSGUB5Wo, user-scope Reseller (91912.service), user/domain: *
(sees every domain under the ACG reseller territory), readonly: no (read-write —
so the --confirm gate on writes matters), can-create-keys: no.
The client never hardcodes secrets. Resolution order: PACKETDIAL_API_KEY env →
PACKETDIAL_CLIENT_ID+friends env → vault credentials.api_key → vault OAuth fields
(client_id/client_secret/username/password). Env overrides exist for quick
testing without touching the vault.
The key's own docs reference
https://api.ucaasnetwork.com/ns-api/...; the skill targetspbx.packetdial.com/ns-api/v2and the key works there directly. They are the same NetSapiens platform behind two white-label hostnames — if a future endpoint 403s onpbx., override withPACKETDIAL_API_BASE_URL=https://api.ucaasnetwork.com/ns-api/v2.
Running the CLI
This machine's Python launcher is py (per identity.json); python / python3
also work. Run from the scripts dir so the two modules resolve.
cd C:/claudetools/.claude/skills/packetdial/scripts
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py status # API version + authenticated key identity
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domains # list all domains
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domain <domain> # one domain's config
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py users <domain> # users / extensions in a domain
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py user <domain> <user>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py phones <domain> # SIP devices registered in a domain
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dids <domain> # phone numbers (DIDs) on a domain
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py devices <domain> <user>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py callqueues <domain> # ACD call queues (agents, live queued count)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py timeframes <domain> # time-based routing (business hours / holidays)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py sites <domain> # multi-site definitions
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py autoattendants <domain> # IVR menus
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py contacts <domain> # shared address book
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py billing <domain> # limits + current usage counts
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain <domain> --start 2026-06-01 --end 2026-06-02
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py resellers
All read wrappers above are live-verified against the production reseller key (2026-06-22).
Writes (gated)
Every mutating command prints a [DRY RUN] line and exits non-zero unless you
pass --confirm. Bodies are raw JSON matching the NetSapiens v2 schema.
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-domain --body '{"domain":"acme","description":"Acme Inc"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-user acme --body '{"user":"101","name-first-name":"Jane"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-phone acme --body '{...}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-did acme --body '{"phonenumber":"15205551234"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py update-user acme 101 --body '{"name-last-name":"Doe"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py delete-user acme 101 --confirm
Call queue edits (live-verified create→update→delete, 2026-06-22)
# create — body MUST include synchronous + callqueue (the queue extension/id)
ns.py create-callqueue acme --body '{"synchronous":"yes","callqueue":"8000","description":"Support"}' --confirm
ns.py update-callqueue acme 8000 --body '{"description":"Support (2nd shift)","callqueue-dispatch-type":"Round-robin"}' --confirm
ns.py delete-callqueue acme 8000 --confirm
# agents on a queue
ns.py add-agent acme 8000 --body '{"callqueue-agent-id":"101@acme"}' --confirm
ns.py update-agent acme 8000 101@acme --body '{"callqueue-agent-dispatch-order-ordinal":1}' --confirm
ns.py remove-agent acme 8000 101@acme --confirm
Time frame edits (live-verified create→update→delete, 2026-06-22)
Timeframes are body-discriminated — same path, the server picks the operation from the
body. The id arg is the timeframe-id (from ns.py timeframes <domain>).
# create: timeframe-type = always | days-of-week | specific-dates | holiday | custom
ns.py create-timeframe acme --body '{"synchronous":"yes","timeframe-name":"After Hours","timeframe-type":"always"}' --confirm
# update: the body carries the type-specific array OR a type-convert:
ns.py update-timeframe acme <timeframe-id> --body '{"update-only":"yes","timeframe-days-of-week-array":[...]}' --confirm
ns.py update-timeframe acme <timeframe-id> --body '{"type":"days-of-week"}' --confirm # convert type
# delete: no body = delete the whole timeframe; {"child_id":N} = delete one entry
ns.py delete-timeframe acme <timeframe-id> --confirm
Gotcha: a days-of-week (or other entry-bearing) timeframe rejects creation without its
entry array (HTTP 400 "...should have at most 1 days of week entry") — create always first
and convert, or include the array in the create body. The arizonacomputerguru domain is the
sanctioned test bed (not in production use) for exercising these writes safely.
Raw escape hatch (any of the 239 v2 paths)
The named commands cover the common surface; for anything else, hit the path
directly. Non-GET methods still require --confirm.
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET domains/acme/users/101/answerrules
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw POST domains/acme/users --body '{...}' --confirm
API capability map (RTFM — spec v44.4.10, mapped 2026-06-22)
The live OpenAPI spec is NetSapiens API v2 v44.4.10 — 239 paths / 354 operations
(GET 139 · POST 87 · PUT 68 · DELETE 50 · PATCH 10). Almost everything is nested under
/domains/{domain}/.... What the platform exposes, and how this skill surfaces it:
Resource (/domains/{domain}/…) |
Ops | Skill wrapper | Notes |
|---|---|---|---|
users (+ /devices, answerrules, voicemail, contacts under a user) |
R/W, biggest surface | users, user, devices, create/update/delete-user |
per-user devices live at users/{u}/devices |
phonenumbers (DIDs + dial-rule routing) |
R/W | dids, create-did |
|
phones (SIP devices, MAC provisioning) |
R/W | phones, create-phone |
|
callqueues (ACD: agents, dispatch, live stats) |
R/W + PATCH | callqueues + create/update/delete-callqueue, add/update/remove-agent |
full CRUD wrapped (verified) |
timeframes (business-hours / holiday schedules) |
R/W | timeframes + create/update/delete-timeframe |
body-discriminated; CRUD wrapped (verified) |
autoattendants (IVR menus) |
R/W | autoattendants (read) |
|
sites (multi-site) |
R/W | sites (read) |
|
contacts (domain address book) |
R/W | contacts (read) |
|
addresses (E911) · moh (music on hold) · dialplans · msg/smsnumbers (SMS) · connections · number-filters |
R/W | raw |
not yet wrapped |
cdrs · calls · recordings · transcriptions · queuedcall |
read | cdrs; rest via raw |
call data |
billing (limits + current counts) |
read | billing |
per-domain quota snapshot |
Top-level: domains, resellers, apikeys, subscriptions, routes, configurations, certificates, templates, jwt, tokens |
R/W | domains/domain/resellers; rest via raw |
platform/admin |
Everything not wrapped is reachable via raw <METHOD> <path> (non-GET gated by
--confirm). The local spec copy used for this map: .claude/tmp/ns-openapi.json (refetch
from pbx.packetdial.com/ns-api/webroot/openapi/openapi.json).
Standard provisioning flow (new customer)
create-domain-> dial plan auto-generatescreate-userper extensioncreate-phoneper SIP device (MAC-provisioned)create-didto attach DIDs and route them to users- Log the work back to the Syncro ticket
Notes
- This is the LIVE production reseller PBX. A bad
create-domainordelete-useraffects real customers — confirm the target domain first with a read command before any write. - CDR queries can be large; always pass
--start/--endand a--limit. - Reference detail (auth shapes, full endpoint inventory) lives in
references/api.md.