Skip to content

Number normalization

Every carrier wants phone numbers in a different shape: Teams Direct Routing wants +E.164, an IMS core wants tel:+E.164, a national PSTN trunk wants the national 0X form, some interconnects want 00-international or bare digits. Instead of hand-rewriting From / To / P-Asserted-Identity / R-URI per call with set_*_user, declare a number policy once and apply it with one call.

The idea

A policy maps each identity header to a target format under a home numbering plan. rewrite_identities() walks the message and reformats every dialable userpart in place — leaving display names, tags, hosts, non-numbers and preserved service codes untouched.

Format Example
e164 +31612345678
plain 31612345678
international 0031612345678
national 0612345678

Config

# siphon.yaml
numbering:
  country_code: "31"           # home country calling code (numeric, 31 = NL)
  trunk_prefix: "0"
  international_prefix: "00"
  assume: national             # a bare 612345678 is a national number

number_policies:
  # Everything canonical +E.164 for the IMS core.
  "ims-e164@2026":
    default: e164

  # What a national PSTN trunk expects on the wire.
  "pstn-national@2026":
    default: national
    headers:
      request-uri: national      # the trunk dials the national form
      From: international         # present CLI as 00-intl
      P-Asserted-Identity: international
    preserve_users: ["112", "0800*"]   # never touch emergency / service codes

script:
  path: "/etc/siphon/edge.py"

Proxy: normalize inbound to the core

from siphon import proxy

@proxy.on_request("INVITE")
def route(request):
    # Whatever shape the trunk sent (00…, 0X, bare), make it +E.164
    # before it hits the IMS core.
    request.rewrite_identities("ims-e164@2026")
    request.relay()

Or inline, without a named policy:

request.rewrite_identities(format="e164")
request.rewrite_identities(format="national", headers=["From", "request-uri"])

B2BUA: normalize per direction

At a trunk↔IMS SBC, normalize the A-leg into canonical +E.164 for your routing logic and CDRs, then format the B-leg for the carrier you dial:

from siphon import b2bua

@b2bua.on_invite
def on_invite(call):
    # ingress: canonicalise whatever the A-leg carrier sent
    call.rewrite_identities("ims-e164@2026")

    dest = call.to_uri          # now guaranteed +E.164 for routing / CDR

    # egress: format the identities and the dial target for the trunk
    call.dial(dest,
              header_policy="sip-trunk-edge@2026",
              number_policy="pstn-national@2026")

number_policy= applies to every branch of call.fork(...) too, and b2bua.default_number_policy sets a default for calls that don't pass one.

Parse a single number

from siphon import numbers

n = numbers.parse("0031612345678")
n.e164        # "+31612345678"
n.national    # "0612345678"
n.cc          # "31"
n.nsn         # "612345678"

Diverting numbers (Diversion / History-Info)

Diversion (RFC 5806) and History-Info (RFC 7044) carry the diverting party's number inside structured, indexed entries. They're off by default. Opt in with a diversion: block — it rewrites only the diverting number per entry and preserves the index, reason, the embedded escaped cause, entry ordering, and any privacy-restricted entry:

number_policies:
  "carrier-x-egress@2026":
    default: e164
    diversion:
      format: e164
      apply_to: [Diversion, History-Info]
      respect_privacy: true      # skip privacy-restricted entries

Testing it

Exercise your script without a running server using the siphon-sip mock SDK:

from siphon_sdk import mock_module
from siphon_sdk.request import Request

mock_module.install()
mock_module.get_numbers().configure(country_code="31")

request = Request(method="INVITE",
                  ruri="sip:0201234567@example.com",
                  from_uri="sip:0612345678@example.com")
request.rewrite_identities(format="e164")

assert request.from_uri.user == "+31612345678"
assert request.ruri.user == "+31201234567"

See the Numbers API reference for the full surface.