packetdial: build out + document the skill against the live NetSapiens v2 API
Key is now provisioned + live-verified, so grounded the skill in the real spec (RTFM): - Mapped the OpenAPI surface (v44.4.10, 239 paths / 354 ops) — capability map added to SKILL.md (what the platform exposes vs what's wrapped vs raw-only). - Added 6 live-verified read wrappers (ns.py + ns_client.py): callqueues, timeframes, sites, contacts, autoattendants, billing (domain limits/usage). - Replaced the stale "not yet provisioned" credentials section with the live status (vaulted nsr_ reseller key, key-id nsr_hSGUB5Wo, scope Reseller 91912.service, RW) + the pbx.packetdial.com vs api.ucaasnetwork.com hostname note + override. - api.md history updated. Writes remain gated behind --confirm; everything unwrapped reachable via `raw`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: packetdial
|
||||
description: "Manage the ACG PacketDial/OITVOIP hosted VoIP via the NetSapiens API (pbx.packetdial.com). List/inspect domains, users, devices, DIDs, resellers; pull CDRs; provision domains/users/SIP/numbers (writes gated --confirm; read-only default). Triggers: packetdial, oitvoip, netsapiens, voip domain/user/extension, provision phone, add did, CDR. Live production PBX."
|
||||
description: "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."
|
||||
|
||||
---
|
||||
|
||||
@@ -23,35 +23,23 @@ every write (create / update / delete) is gated behind `--confirm`.
|
||||
- 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 — ONE-TIME SETUP (not yet provisioned)
|
||||
## Credentials — PROVISIONED + LIVE-VERIFIED (2026-06-22)
|
||||
|
||||
As of this skill's creation **no API key exists yet** — the vault entry
|
||||
`msp-tools/oitvoip.sops.yaml` is empty/absent, so every command will fail with a
|
||||
clear "No credentials found" error until you do this once:
|
||||
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`.
|
||||
|
||||
1. Log into `pbx.packetdial.com` -> **Admin > API Keys** and create a
|
||||
reseller-scoped key (prefix `nsr_`). If self-service key creation is not
|
||||
available, reply to **Darwin Escaro (OITVOIP)** for reseller OAuth client
|
||||
credentials.
|
||||
2. Store it in the SOPS vault. Preferred (static bearer key):
|
||||
```
|
||||
# msp-tools/oitvoip.sops.yaml
|
||||
credentials:
|
||||
api_key: nsr_xxxxxxxxxxxxxxxx
|
||||
```
|
||||
Or, for OAuth2 password-grant credentials:
|
||||
```
|
||||
credentials:
|
||||
client_id: <client id>
|
||||
client_secret: <client secret>
|
||||
username: <portal user@domain>
|
||||
password: <portal password>
|
||||
```
|
||||
3. That's it — the client auto-detects which shape is present.
|
||||
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 client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env
|
||||
-> `PACKETDIAL_CLIENT_ID`+friends env -> vault `credentials.api_key` -> vault
|
||||
OAuth fields. Env overrides exist for quick testing without touching the vault.
|
||||
> The key's own docs reference `https://api.ucaasnetwork.com/ns-api/...`; the skill
|
||||
> targets `pbx.packetdial.com/ns-api/v2` and the key works there directly. They are the
|
||||
> same NetSapiens platform behind two white-label hostnames — if a future endpoint 403s
|
||||
> on `pbx.`, override with `PACKETDIAL_API_BASE_URL=https://api.ucaasnetwork.com/ns-api/v2`.
|
||||
|
||||
## Running the CLI
|
||||
|
||||
@@ -69,10 +57,19 @@ 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
|
||||
@@ -97,6 +94,31 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET domains/acme/users
|
||||
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` (read) | writes via `raw` |
|
||||
| `timeframes` (business-hours / holiday schedules) | R/W | `timeframes` (read) | writes via `raw` |
|
||||
| `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)
|
||||
|
||||
1. `create-domain` -> dial plan auto-generates
|
||||
|
||||
@@ -80,3 +80,9 @@ exact request/response schema of any specific path.
|
||||
`msp-tools/oitvoip.sops.yaml` was the open TODO.
|
||||
- 2026-06-02: `packetdial` skill created wrapping the v2 API (read-by-default,
|
||||
gated writes). Confirmed `voip.` is portal-only and `pbx.` is the API host.
|
||||
- 2026-06-22: **Reseller API key provisioned + vaulted** (`msp-tools/oitvoip.sops.yaml`,
|
||||
key-id `nsr_hSGUB5Wo`, scope Reseller `91912.service`, read-write). Live-verified end to
|
||||
end. RTFM pass over the v2 spec (v44.4.10, 239 paths / 354 ops) — capability map added to
|
||||
SKILL.md. Added live-verified read wrappers: `callqueues`, `timeframes`, `sites`,
|
||||
`contacts`, `autoattendants`, `billing`. Reseller territory = 3 domains today
|
||||
(arizonacomputerguru + two `*.91912.service`).
|
||||
|
||||
@@ -97,6 +97,12 @@ def main(argv=None) -> int:
|
||||
sp = sub.add_parser("phones", help="phones (devices) in a domain"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("dids", help="phone numbers in a domain"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("devices", help="devices for a user"); sp.add_argument("domain"); sp.add_argument("user")
|
||||
sp = sub.add_parser("callqueues", help="ACD call queues in a domain"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("timeframes", help="time-based routing schedules in a domain"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("sites", help="multi-site definitions in a domain"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("contacts", help="shared/domain contacts"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("autoattendants", help="auto-attendants (IVR) in a domain"); sp.add_argument("domain")
|
||||
sp = sub.add_parser("billing", help="domain limits + current usage counts"); sp.add_argument("domain")
|
||||
sub.add_parser("resellers", help="list resellers")
|
||||
sp = sub.add_parser("cdrs", help="call detail records")
|
||||
sp.add_argument("--domain"); sp.add_argument("--start"); sp.add_argument("--end")
|
||||
@@ -138,6 +144,18 @@ def main(argv=None) -> int:
|
||||
_emit(client.phonenumbers(args.domain))
|
||||
elif args.cmd == "devices":
|
||||
_emit(client.devices(args.domain, args.user))
|
||||
elif args.cmd == "callqueues":
|
||||
_emit(client.callqueues(args.domain))
|
||||
elif args.cmd == "timeframes":
|
||||
_emit(client.timeframes(args.domain))
|
||||
elif args.cmd == "sites":
|
||||
_emit(client.sites(args.domain))
|
||||
elif args.cmd == "contacts":
|
||||
_emit(client.contacts(args.domain))
|
||||
elif args.cmd == "autoattendants":
|
||||
_emit(client.autoattendants(args.domain))
|
||||
elif args.cmd == "billing":
|
||||
_emit(client.billing(args.domain))
|
||||
elif args.cmd == "resellers":
|
||||
_emit(client.resellers())
|
||||
elif args.cmd == "cdrs":
|
||||
|
||||
@@ -351,6 +351,31 @@ class NetSapiensClient:
|
||||
def subscriptions(self) -> Any:
|
||||
return self.request("GET", "subscriptions")
|
||||
|
||||
# --- per-domain feature resources (live-verified shapes, 2026-06-22) ---
|
||||
def callqueues(self, domain: str) -> Any:
|
||||
"""ACD call queues in a domain (agents, dispatch type, live queued count)."""
|
||||
return self.request("GET", f"domains/{urllib.parse.quote(domain)}/callqueues")
|
||||
|
||||
def timeframes(self, domain: str) -> Any:
|
||||
"""Time-based routing schedules (business hours / holidays) for a domain."""
|
||||
return self.request("GET", f"domains/{urllib.parse.quote(domain)}/timeframes")
|
||||
|
||||
def sites(self, domain: str) -> Any:
|
||||
"""Multi-site definitions within a domain."""
|
||||
return self.request("GET", f"domains/{urllib.parse.quote(domain)}/sites")
|
||||
|
||||
def contacts(self, domain: str) -> Any:
|
||||
"""Shared/domain contacts (address book)."""
|
||||
return self.request("GET", f"domains/{urllib.parse.quote(domain)}/contacts")
|
||||
|
||||
def autoattendants(self, domain: str) -> Any:
|
||||
"""Auto-attendants (IVR menus) in a domain."""
|
||||
return self.request("GET", f"domains/{urllib.parse.quote(domain)}/autoattendants")
|
||||
|
||||
def billing(self, domain: str) -> Any:
|
||||
"""Domain billing/limits snapshot: max + current counts (users, queues, AAs, calls)."""
|
||||
return self.request("GET", f"domains/{urllib.parse.quote(domain)}/billing")
|
||||
|
||||
# ======================================================================
|
||||
# WRITE METHODS (gated — the CLI requires --confirm before calling these)
|
||||
# ======================================================================
|
||||
|
||||
Reference in New Issue
Block a user