Skip to content

Outbound Dialplan Normalization

Reference for the number-format normalization rules that the dialplan generator emits at the top of every org_<prefix>__outbound context. These rules sit BEFORE the route-specific _X. pattern and rewrite incoming digit strings into the format the upstream trunk expects.

Why this exists

Customers dial numbers in many formats:

  • 9944421125 — bare 10-digit subscriber number
  • 919944421125 — with India country code, no plus
  • +919944421125 — full E.164
  • 09944421125 — with national-trunk prefix (rare in modern softphones, but legacy PSTN habit)

All four should reach the trunk identically, because:

  • Tata's outbound termination accepts only the 10-digit form
  • Softphones render incoming CallerID inconsistently (Zoiper uses 12-digit, native iOS Phone may use 10-digit)
  • Tap-to-call from any of those formats should succeed without the agent retyping

Rules emitted today

Generator: api/src/services/asterisk/dialplanGenerator.jsgenerateOutboundContext(org). Rules emit only for orgs that have at least one active outbound route.

1. Strip leading + (E.164 normalization)

; Normalize E.164 leading '+'
exten => _+X.,1,NoOp(Stripping leading + from ${EXTEN})
exten => _+X.,n,Goto(${EXTEN:1},1)

Matches +<anything>. Goto(${EXTEN:1},1) re-enters the same context with the leading + removed, so subsequent rules pick up the digits-only form.

2. Strip Indian country code 91 (12-digit input)

; Normalize Indian country code '91' (Tata trunk expects 10-digit)
exten => _91XXXXXXXXXX,1,NoOp(Stripping 91 country code from ${EXTEN})
exten => _91XXXXXXXXXX,n,Goto(${EXTEN:2},1)

Matches exactly 12 digits starting with 91. Re-enters with the first two characters stripped (${EXTEN:2}), leaving the bare 10-digit number that _X. will catch.

3. Catch-all _X. route

exten => _X.,1,NoOp(Outbound call via Default Outbound)
exten => _X.,n,Set(__ORG_ID=...)
exten => _X.,n,Set(CALLERID(num)=...)         ; org's outbound CID
...
exten => _X.,n,Dial(PJSIP/${EXTEN}@<trunk>,60)

_X. matches one or more digits. By the time we reach this rule, prior rules have normalised the input to bare digits.

How matches resolve (Asterisk rule precedence)

Asterisk picks the most specific pattern that matches. Specificity is roughly: literal characters > character ranges > wildcards.

Input Most specific match Net effect
9944421125 _X. Dialled directly (10-digit)
919944421125 _91XXXXXXXXXX (more specific than _X.) Strip → re-enter as 9944421125_X. Dials
+919944421125 _+X. Strip + → re-enter as 919944421125 → above row applies
+9944421125 _+X. Strip + → re-enter as 9944421125_X. Dials
1001 (extension) (in _internal context, exact 1001 rule) Calls user 1001, never reaches _outbound
5001 (queue) (in _queue context, exact 5001 rule) Enters queue, never reaches _outbound

Adding a new normalization rule

Use the Goto(${EXTEN:N},1) pattern — matches digits, strips, re-enters the context so a more general rule catches the result. Keep patterns narrow:

  • _91XXXXXXXXXX (12 digits, starts with 91) — safe
  • _91X. (anything starting with 91) — would swallow legitimate extension dials starting with 91 if any existed
  • _NXX. (anything starting with non-zero) — too broad, would conflict with _X.

Example — strip the legacy national-trunk prefix 0 from 11-digit dials

If a customer's softphone produces 09944421125, you'd add:

; Strip legacy national-trunk leading '0'
exten => _0XXXXXXXXXX,1,NoOp(Stripping leading 0 from ${EXTEN})
exten => _0XXXXXXXXXX,n,Goto(${EXTEN:1},1)

Place before the 91 strip and the _X. catch-all. Re-deploy + regenerate.

When the rules DON'T apply

These contexts run normalization independently:

  • org_<prefix>__internal — extension dials (1001-9999), conferences (_8XXX), call-forward (_*72.), etc. — different patterns; not affected.
  • org_<prefix>__incoming — inbound trunk traffic; CallerID is what the carrier sent, not normalized further.
  • org_<prefix>__queue — queue numbers (e.g., 5001); specific match, no normalization.
  • tata-did-route / tata-inbound — gateway dispatcher contexts on PROD; carrier-side handling.

Trunk-specific format expectations

Trunk type Upstream Expected dial format
Tata via NUC (all current customer trunks) sip:10.10.10.2:5060 10-digit subscriber, no country code
Tata gateway (system) sip:10.10.10.2:5060 Same
(future) direct ITSP requiring E.164 TBD Would need a per-trunk preserve_country_code flag on OutboundRoute so generator skips the 91 strip for that route

Today every customer routes through Tata, so the 91 strip is universally correct.

Re-deploying after a generator change

A change in dialplanGenerator.js only affects new generations — existing org config files on disk keep the old behaviour until re-deployed. After updating the generator on PROD:

ssh root@89.116.31.109
cd /opt/astrapbx

# Regenerate dialplans for all active orgs + reload Asterisk
node -e '
(async () => {
  const ConfigDeploymentService = require("./src/services/asterisk/configDeploymentService");
  const inst = new ConfigDeploymentService();
  const { Organization } = require("./src/models");
  const orgs = await Organization.findAll({ where: { status: "active" } });
  console.log("orgs to regenerate:", orgs.length);
  for (const o of orgs) {
    process.stdout.write("  - " + o.name + " ... ");
    try { await inst.deployOrganizationConfiguration(o.id, o.name); console.log("ok"); }
    catch (e) { console.log("FAIL: " + e.message); }
  }
  await inst.reloadAsteriskConfiguration();
  console.log("reloaded");
  process.exit(0);
})().catch(e => { console.error(e.message); process.exit(1); });
'

Verify with dialplan show <number>@<org>__outbound afterward.

See also