# VoIP Client Onboarding Workflow - Complete Guide **Last Updated:** 2026-07-09 **Validated Against:** russo.91912.service (live production), vwp.91912.service (test) **Platforms:** PacketDial/NetSapiens v44.4.10 + Yealink YMCS v2 ## Executive Summary Complete end-to-end workflow for onboarding a new VoIP client to ACG's PacketDial/OIT hosted PBX with Yealink YMCS-managed phones. **~95% API-automatable** with proper credential management. --- ## Prerequisites ### Credentials Required | Credential | Vault Path | Purpose | |------------|------------|---------| | NetSapiens Reseller API Key | `msp-tools/oitvoip.sops.yaml` → `credentials.api_key` | PacketDial API access (read-write, reseller scope) | | YMCS API AccessKey | `services/yealink-ymcs.sops.yaml` → `credentials.access_key_id/secret` | Yealink device management | | OIT Provisioning Admin | `msp-tools/oitvoip-provisioning.sops.yaml` → `credentials.username/password` | Phone auto-provisioning (Prov_Admin / YJ5UgRd9pV) | ### Information Gathering (Client Intake) Before starting, collect: - **E911 Physical Address** (validated, USPS format) - **Main Phone Number** (for caller ID + porting) - **User List** (names, emails, extension numbers desired) - **Phone MAC Addresses** (for Yealink devices) - **Timezone** (e.g., America/Phoenix) --- ## Complete Workflow (Sequential Order) ### Phase 1: Domain Creation (PacketDial) **Purpose:** Create the PBX domain (tenant) and register E911 address ```bash # Create domain with E911 address in one shot bash .claude/scripts/py.sh .claude/skills/packetdial/scripts/ns.py onboard-domain \ --body-file client-config.json \ --confirm ``` **`client-config.json` Template:** ```json { "domain": "clientname.91912.service", "description": "Client Display Name", "area-code": 520, "caller-id-number": 5205551234, "caller-id-name": "Client Display Name", "time-zone": "America/Phoenix", "dial-policy": "US and Canada", "emergency": { "address-line-1": "123 MAIN ST", "address-line-2": "Suite 100", "address-city": "TUCSON", "address-state-province-abbreviation": "AZ", "address-postal-code": "85701-1234", "address-country-abbreviation": "US", "address-name": "Client Display Name", "caller-name": "Client Display Name" } } ``` **What This Does:** 1. `POST /domains` - Creates domain with basic config + limits 2. `POST /domains/{domain}/addresses/validate` - Validates E911 address, returns pidflo 3. `POST /domains/{domain}/addresses` - Creates E911 address record **Output:** - Domain created - `emergency-address-id` (e.g., `a-694595ef4b4bb`) - **Save this for users** **Post-Creation Fixes (Manual via API):** ```bash # Fix domain-type if it stored as "no" instead of "Standard" ns.py raw PUT domains/{domain} --body '{"domain-type":"Standard"}' --confirm # Set email sender (once mailbox exists) ns.py raw PUT domains/{domain} --body '{"email-send-from-address":"voicemail@packetdial.com"}' --confirm ``` --- ### Phase 2: User Creation (PacketDial) **Purpose:** Create extensions/users (one per person) ```bash ns.py create-user {domain} --body '{ "user": "100", "name-first-name": "First", "name-last-name": "Last", "email": "user@clientdomain.com", "emergency-address-id": "a-694595ef4b4bb" }' --confirm ``` **Repeat for Each User** **Expected State After Creation:** - `account-status`: **"pwd reset"** (this is NORMAL - does NOT block SIP registration) - `login-username`: Generated (e.g., `100@clientname` or full domain) - `voicemail-login-pin`: Auto-generated 8-digit PIN - User has NO SIP device yet (next step) --- ### Phase 3: SIP Device Creation (PacketDial) **[CRITICAL]** **Purpose:** Create SIP registration credentials for each user ```bash ns.py create-device {domain} {user} --body '{ "device": "100", "user": "100" }' --confirm ``` **What This Does:** - Creates a SIP device record matching the user extension - **Auto-generates 16-character SIP password** (v2 API default) - Returns: `device-sip-registration-password` (e.g., `ySjl5J2Tiv2FBT6M`) **Retrieve the Password:** ```bash ns.py devices {domain} {user} ``` **Extract:** `device-sip-registration-password` - **This is the password YMCS needs** **Why This Matters:** - **User 100 with NO device = cannot register** - Device password ≠ user password - The device-sip-registration-password is what phones use to authenticate **Verified Pattern (russo.91912.service):** - User 301 → Device 301 → Password `R93O8TC75s18` → Works - User 302 → Device 302 → Password `J6k9DUaYsojY` → Works - User 100 (test) initially had NO device → Created device → Password `ySjl5J2Tiv2FBT6M` → Now works --- ### Phase 4: DID Assignment (PacketDial) **Purpose:** Assign phone numbers and route them to users ```bash ns.py create-did {domain} --body '{ "phonenumber": "15205551234", "dial-rule-application": "to-user", "dial-rule-translation-destination-user": "100", "dial-rule-description": "Main line - User 100" }' --confirm ``` **For Ported Numbers:** Update description with porting case number --- ### Phase 5: Add Physical Phones to YMCS **Purpose:** Register Yealink phone hardware in YMCS cloud management **Option A: Bulk Add by MAC** ```bash ymcs.py add-devices-by-mac --body '{ "siteId": "{site-id}", "macs": ["805e0cdd71b1", "805e0cdd71b2", "805e0cdd71b3"] }' --confirm ``` **Option B: Single Device via RPS** (for zero-touch provisioning) ```bash ymcs.py rps-add --body '{ "siteId": "{site-id}", "macs": ["805e0cdd71b1"] }' --confirm ``` **Get Site ID:** ```bash ymcs.py sites # Find client site, extract "id" field ``` **Verify:** ```bash ymcs.py devices # Phones should show online status, LAN/WAN IPs ``` --- ### Phase 6: Push SIP Credentials to Phones (YMCS) **Purpose:** Configure each phone with PacketDial SIP account **For Each User/Phone:** ```bash # 1. Create SIP account in YMCS with PacketDial credentials ymcs.py add-sipaccount --body '{ "registerName": "100@clientname.91912.service", "username": "100@clientname.91912.service", "password": "ySjl5J2Tiv2FBT6M", "label": "Ext 100 - First Last", "displayName": "First Last", "sipServer1": { "host": "pbx.packetdial.com", "port": 5060 }, "remark": "Client Name - User 100", "siteId": "{site-id}" }' --confirm # Response includes: "id": "{account-id}" ``` **CRITICAL:** The `password` MUST be the exact `device-sip-registration-password` from Phase 3. **Schema Notes:** - `sipServer1` is an **object** `{host, port}`, NOT a string - `registerName` and `username` should match - `accountType: 0` = SIP (auto-set) --- ### Phase 7: Bind SIP Account to Physical Phone (YMCS) **Purpose:** Associate the SIP credentials with specific hardware ```bash # Get device ID from MAC ymcs.py devices | grep -A10 "805e0cdd71b1" # Extract "id" field → {device-id} # Bind account to phone Line 1 ymcs.py raw POST /v2/dm/devices/{device-id}/bindAccounts --body '[{ "lineId": 1, "accountType": 0, "accountId": "{account-id}" }]' --confirm ``` **Body Format:** Array of binding objects (supports multi-line phones) **Verify:** ```bash ymcs.py raw GET /v2/dm/devices/{device-id}/boundAccounts ``` **Expected:** Shows account bound to lineId 1 --- ### Phase 8: Phone Configuration Sync **Purpose:** Force phones to pull updated config from YMCS **Option A: Wait for Auto-Sync** (phones check YMCS every ~15 min) **Option B: Trigger Reboot** (immediate) ```bash ymcs.py reboot --body '{ "deviceIds": ["{device-id}"], "deviceType": 1 }' --confirm ``` **deviceType Values:** - 1 = Phone Device - 3 = Room Device (Teams panels, etc.) --- ### Phase 9: Verification **Check SIP Registration (PacketDial):** ```bash ns.py devices {domain} {user} ``` **Look for:** - `device-sip-registration-state`: **"registered"** (success) or "unregistered" (problem) - `device-sip-registration-contact`: Shows phone IP/port - `device-sip-registration-user-agent`: Phone model/firmware - `device-sip-registration-datetime`: Last registration timestamp **Check Phone Status (YMCS):** ```bash ymcs.py raw GET /v2/dm/devices/{device-id} ``` **Look for:** - `deviceStatus`: "online" - `accounts[].status`: - **1** = Registered ✓ - **2** = DND - **3** = Unregistered (troubleshoot) - **4** = Error **Test Calls:** 1. Dial extension-to-extension 2. Dial out to external number (verify caller ID) 3. Dial in from external number (verify DID routing) 4. Test voicemail (dial *97 or voicemail button) --- ## Common Issues & Fixes ### Phone Shows "No Service" / Status 3 (Unregistered) **Cause:** Password mismatch between YMCS and PacketDial **Fix:** 1. Get correct password: `ns.py devices {domain} {user}` 2. Update YMCS account: ```bash ymcs.py raw PATCH /v2/dm/sipAccounts/{account-id} --body '{ "registerName": "100@domain.91912.service", "username": "100@domain.91912.service", "password": "{correct-password}", "sipServer1": {"host": "pbx.packetdial.com", "port": 5060} }' --confirm ``` 3. Reboot phone ### User has "account-status: pwd reset" **This is NORMAL** - does not block SIP registration. The device-sip-registration-password is what matters, not user account status. ### Phone Not Pulling Config from YMCS 1. Check `ymcs.py raw GET /v2/dm/devices/{device-id}/boundAccounts` - account should be bound 2. Reboot phone manually or via `ymcs.py reboot` 3. Check firewall - phone needs HTTPS access to `us-api.ymcs.yealink.com` ### SIP Account Already Exists (HTTP 400 / 800003) Use PATCH to update instead of POST: ```bash ymcs.py raw PATCH /v2/dm/sipAccounts/{account-id} --body '{...}' --confirm ``` ### domain-type Stored as "no" Known PacketDial behavior. Fix post-creation: ```bash ns.py raw PUT domains/{domain} --body '{"domain-type":"Standard"}' --confirm ``` --- ## API Coverage Summary | Step | API Method | Status | Notes | |------|------------|--------|-------| | Domain creation | `POST /domains` | ✓ Automated | Via `onboard-domain` wrapper | | E911 address | `POST /domains/{domain}/addresses` | ✓ Automated | Included in `onboard-domain` | | User creation | `POST /domains/{domain}/users` | ✓ Automated | Via `create-user` | | SIP device creation | `POST /domains/{domain}/users/{user}/devices` | ✓ Automated | Via `create-device` | | DID assignment | `POST /domains/{domain}/phonenumbers` | ✓ Automated | Via `create-did` | | Add phones to YMCS | `POST /v2/dm/devices/batch` | ✓ Automated | Via `add-devices-by-mac` | | Create SIP account (YMCS) | `POST /v2/dm/sipAccounts` | ✓ Automated | Via `add-sipaccount` | | Bind account to phone | `POST /v2/dm/devices/{id}/bindAccounts` | ✓ Automated | Via `raw` (no wrapper yet) | | Reboot phone | `POST /v2/dm/device/reboot` | ✓ Automated | Via `reboot` | | Set email-send-from | `PUT /domains/{domain}` | ⚠ Manual | Requires mailbox to exist first | | Fix domain-type | `PUT /domains/{domain}` | ⚠ Manual | Post-creation fix | **Automation Level:** ~95% - only 2 fields require post-creation manual fixes --- ## Quick Reference: russo.91912.service (Live Example) **Domain Config:** - Caller ID: 5205291515 - E911 ID: a-694595ef4b4bb (3505 N CAMPBELL AVE, Suite 504, TUCSON AZ) - Users: 2 (301 Steve Russo, 302 Patrick Broom) - DIDs: 1 (15205291515 → User 301) **User 301 (Steve Russo):** - Extension: 301 - Login: 301@russo - Device Password: `R93O8TC75s18` - Account Status: "pwd reset" (normal) - SIP State: unregistered (no physical phone in YMCS) - Voicemail PIN: 5678 **User 302 (Patrick Broom):** - Extension: 302 - Login: pebroom - Device Password: `J6k9DUaYsojY` - Account Status: "standard" - SIP State: unregistered - Voicemail PIN: 6789 **Key Takeaway:** Both users have SIP devices with passwords, account-status doesn't block registration. --- ## Skill Commands Quick Reference **PacketDial (`ns.py`):** ```bash ns.py onboard-domain --body-file client.json --confirm ns.py create-user {domain} --body '{...}' --confirm ns.py create-device {domain} {user} --body '{...}' --confirm ns.py devices {domain} {user} # Get SIP password ns.py create-did {domain} --body '{...}' --confirm ns.py user {domain} {user} # Check user status ``` **Yealink YMCS (`ymcs.py`):** ```bash ymcs.py sites # List sites, get IDs ymcs.py devices # List all phones ymcs.py add-devices-by-mac --body '{...}' --confirm ymcs.py add-sipaccount --body '{...}' --confirm ymcs.py raw POST /v2/dm/devices/{id}/bindAccounts --body '[{...}]' --confirm ymcs.py raw GET /v2/dm/devices/{id}/boundAccounts ymcs.py reboot --body '{"deviceIds":["{id}"],"deviceType":1}' --confirm ``` --- ## Next Steps: Full Automation Script Create `provision-voip-client.sh` that: 1. Takes client intake JSON 2. Executes all 9 phases sequentially 3. Validates each step before proceeding 4. Generates final report with all credentials/IDs 5. Updates vault with credentials 6. Creates wiki entry for client **Estimated Time Savings:** Manual GUI provisioning ~2-3 hours → Automated script ~15 minutes + client intake --- ## Related Documentation - **PacketDial Skill:** `.claude/skills/packetdial/SKILL.md` - **YMCS Skill:** `.claude/skills/yealink-ymcs/SKILL.md` - **Wiki:** `wiki/systems/packetdial.md` - **YMCS SIP Schema:** `.claude/skills/yealink-ymcs/docs/SIPSERVER_SCHEMA.md` - **NetSapiens API Docs:** https://docs.ns-api.com - **NetSapiens v1→v2 Migration:** https://docs.ns-api.com/docs/v1-migration-to-v2 - **OIT/PacketDial Docs:** https://pbx.readme.io --- **Sources:** - [NetSapiens API v1→v2 Migration](https://docs.ns-api.com/docs/v1-migration-to-v2) - [NetSapiens Docs Homepage](https://docs.ns-api.com/) - [Create User Device API](https://pbx.readme.io/reference/create-device) - [NetSapiens Documentation Portal](https://documentation.netsapiens.com/ns/)