Bulk-Create Phone-Routed Users (Vendor Lists)¶
Create many "Phone Call" extensions at once from a spreadsheet — e.g. a hospital's vendor/supplier directory where each extension just forwards to an external landline/mobile. Worked example: Salem Gopi Hospital — 30 vendor extensions (
01–30) created 2026-07-07 in one API call.For a single user, just use the editor's Add User dialog. This runbook is for 10+ rows where the batch API is faster and less error-prone.
What "Phone Call" routing actually is¶
There is no phone routing_type. A "Phone Call (Mobile)" user in the editor is:
routing_type = 'sip'(the default — every user gets an auto-generated SIP endpoint, unused here)ring_target = 'phone'phone_number = <the external number>
Inbound calls to that extension Dial(PJSIP/<number>@<org>_trunk...) out via the org's Tata trunk, presenting the org DID as caller ID.
Phone number format — store bare 10 digits¶
The dialplan strips phone_number to its last 10 digits before dialing (dialplanGenerator.js, phoneNum.slice(-10)). So for Indian numbers:
| Sheet value | Store as | Why |
|---|---|---|
044-24912122 (Chennai landline) | 4424912122 | STD 044 minus the trunk 0 = 10-digit national number |
0427 2451801 (Salem landline) | 4272451801 | same — drop the leading 0 |
9500273694 (mobile) | 9500273694 | already 10 digits |
+91 98427 09297 | 9842709297 | strip +91, spaces |
Normalization rule: strip all non-digits, take the last 10. STD calls connect without the leading 0 (verified on Thangavelu ext 1009 "Landline" 4298230669). Storing with 0/+91 also works (dialplan trims anyway), but bare 10 digits is the house style.
Step 1 — Parse the spreadsheet¶
openpyxl may choke on some xlsx stylesheets (IndexError: list index out of range). Fallback: read the raw XML from the zip (xl/worksheets/sheet1.xml + xl/sharedStrings.xml).
Step 2 — Check for extension collisions¶
Extensions are unique per org. Before creating, list the org's existing extensions and make sure none of your codes clash:
In the Gopi run, vendor code 11 collided with an existing ext 11 — it was deleted in the editor first. Leading-zero extensions (01–09) are safe: Asterisk matches literal extensions before pattern/outbound routes (_X.), so they don't shadow outbound dialing.
Step 3 — Build the batch payload¶
POST /api/v1/users/batch accepts up to 200 ops. Each create op:
{"op":"create","data":{"extension":"01","full_name":"AJANTHA FABRICS","ring_target":"phone","phone_number":"4274051004"}}
Wrap them with the target org:
routing_type is omitted (defaults to sip); email/password/SIP creds auto-generate. The batch validates all rows first (dupes, unknown fields) and returns 400 with per-row errors[] before writing anything, then applies and does one dialplan deploy at the end.
Step 4 — Execute against the prod API¶
The API is the astrapbx pm2 process on 127.0.0.1:8000. Authenticate server-to-server with the internal key (in /opt/astrapbx/.env, INTERNAL_API_KEY) plus org_id in the body — never print the key:
scp payload.json root@147.93.168.216:/tmp/payload.json
ssh root@147.93.168.216 '
KEY=$(grep "^INTERNAL_API_KEY=" /opt/astrapbx/.env | cut -d= -f2-)
curl -s -X POST http://127.0.0.1:8000/api/v1/users/batch \
-H "Content-Type: application/json" -H "x-internal-key: $KEY" \
--data @/tmp/payload.json
rm -f /tmp/payload.json'
Success looks like {"created":[...30...],"updated":[],"applyErrors":[],"errors":[]}.
Step 5 — Verify¶
ssh root@147.93.168.216 "grep -A2 '^exten => 01,' /etc/asterisk/ext_<org>.conf"
# exten => 01,1,NoOp(Calling AJANTHA FABRICS)
# exten => 01,n,Dial(Local/fwd_01@...)
# exten => 01,n,Dial(PJSIP/4274051004@<org>_trunk...,30,tT)
Refresh the editor Users page — the new extensions show Routing = Phone.
Gotchas¶
- Duplicate numbers across rows are allowed and often legitimate (same distributor group). Flag them to the customer but don't block — the sheet is the source of truth.
org_id(UUID) is in the editor URL:editor.astradial.com/dashboard/<ORG_UUID>/users. It is not the Asteriskcontext_prefix(org_xxxx).- Phone-routed users still get a SIP endpoint + AOR they never register to; harmless. Their qualify state shows offline — expected, no device is meant to register.