Skip to content

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 (0130) 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:

ssh root@147.93.168.216 "asterisk -rx 'pjsip show endpoints' | grep -oE '/[0-9]+' | tr -d /"

In the Gopi run, vendor code 11 collided with an existing ext 11 — it was deleted in the editor first. Leading-zero extensions (0109) 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:

{"org_id":"<ORG_UUID>","operations":[ ... ]}

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 Asterisk context_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.

See also