Author: Mike Swanson Machine: Mikes-MacBook-Air.local Timestamp: 2026-07-10 08:30:31
14 KiB
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
# 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:
{
"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:
POST /domains- Creates domain with basic config + limitsPOST /domains/{domain}/addresses/validate- Validates E911 address, returns pidfloPOST /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):
# 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)
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@clientnameor 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
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:
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
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
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)
ymcs.py rps-add --body '{
"siteId": "{site-id}",
"macs": ["805e0cdd71b1"]
}' --confirm
Get Site ID:
ymcs.py sites
# Find client site, extract "id" field
Verify:
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:
# 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:
sipServer1is an object{host, port}, NOT a stringregisterNameandusernameshould matchaccountType: 0= SIP (auto-set)
Phase 7: Bind SIP Account to Physical Phone (YMCS)
Purpose: Associate the SIP credentials with specific hardware
# 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:
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)
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):
ns.py devices {domain} {user}
Look for:
device-sip-registration-state: "registered" (success) or "unregistered" (problem)device-sip-registration-contact: Shows phone IP/portdevice-sip-registration-user-agent: Phone model/firmwaredevice-sip-registration-datetime: Last registration timestamp
Check Phone Status (YMCS):
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:
- Dial extension-to-extension
- Dial out to external number (verify caller ID)
- Dial in from external number (verify DID routing)
- 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:
- Get correct password:
ns.py devices {domain} {user} - Update YMCS account:
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 - 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
- Check
ymcs.py raw GET /v2/dm/devices/{device-id}/boundAccounts- account should be bound - Reboot phone manually or via
ymcs.py reboot - 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:
ymcs.py raw PATCH /v2/dm/sipAccounts/{account-id} --body '{...}' --confirm
domain-type Stored as "no"
Known PacketDial behavior. Fix post-creation:
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):
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):
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:
- Takes client intake JSON
- Executes all 9 phases sequentially
- Validates each step before proceeding
- Generates final report with all credentials/IDs
- Updates vault with credentials
- 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: