Skip to content

Numbers (E.164 normalization)

The numbers namespace and rewrite_identities() reformat the dialable userpart of every identity header into a single target E.164 shape, so a call leaves siphon in exactly the format the next hop expects (Teams wants +E.164, an IMS core wants tel:+E.164, a national trunk wants 0X, and so on).

from siphon import numbers

n = numbers.parse("0031612345678")
n.e164          # "+31612345678"
n.national      # "0612345678"
n.format("plain")   # "31612345678"

One call rewrites the whole message. Display names, tags, hosts, non-numbers and preserved service/emergency codes are left untouched.

@proxy.on_request("INVITE")
def route(request):
    request.rewrite_identities("teams-outbound@2026")   # named policy
    request.relay()

Formats

Token Example Meaning
e164 +31612345678 leading +, country code, NSN
plain 31612345678 E.164 digits, no +
international 0031612345678 international access prefix + digits
national 0612345678 national trunk prefix + NSN (home country only; a foreign number falls back to the international access form)

Home numbering plan

Parsing and formatting are driven by the numbering: config block — the home country calling code plus the trunk / international prefixes:

numbering:
  country_code: "31"           # numeric calling code (31 = NL), not an ISO code
  trunk_prefix: "0"
  international_prefix: "00"
  assume: national             # how to read a bare, prefix-less number
  min_national_digits: 5       # shorter bare numbers are treated as short codes

Named policies

Policies are named, versioned presets in number_policies:, referenced by request.rewrite_identities("<name>") or call.dial(number_policy="<name>"). Pin the @version so a siphon upgrade never silently shifts the set of headers being reformatted.

number_policies:
  "teams-outbound@2026":
    default: e164
    headers:                     # per-header overrides on top of default
      request-uri: e164
      P-Asserted-Identity: e164

  "pstn-national@2026":
    default: national
    headers:
      request-uri: national      # what the trunk dials
      From: international         # CLI presented as 00-intl
    on_unparseable: keep         # keep | strip (P-headers only)
    preserve_users: ["112", "911", "0800*"]
    diversion:                   # opt-in — see below
      format: e164

The diversion family

Diversion (RFC 5806) and History-Info (RFC 7044) carry the diverting-party number inside structured, multi-valued, indexed entries — a History-Info URI embeds an escaped cause. They are off by default and handled by a separate opt-in diversion: block that rewrites only the userpart of each entry, preserving index, reason, the embedded cause, entry ordering, and any privacy-restricted entry (respect_privacy: true, the default).

numbers namespace

Mock numbers namespace (mirrors the Rust NumbersNamespace).

configure

configure(
    default_number_policy: Optional[str] = None, **kwargs
) -> None

Set the home numbering plan for tests (country_code, trunk_prefix, international_prefix, assume, min_national_digits), and optionally the B2BUA default policy (default_number_policy).

register_policy

register_policy(name: str, **spec) -> None

Register a named policy for tests. Accepts default, headers (dict), walk (list), on_unparseable, preserve_users, country_code.

parse

parse(raw: str, home: Optional[str] = None) -> Number

Parse a raw number string into a :class:Number.

Raises ValueError when the string is not a dialable number.

policy_names

policy_names() -> List[str]

Names of the configured number policies.

Parsed number

Returned by numbers.parse(...).

A parsed telephone number — format it into any E.164 shape.

e164 property

e164: str

Global E.164 with a leading + (e.g. +31612345678).

plain property

plain: str

E.164 digits, no + (e.g. 31612345678).

international property

international: str

International access form (e.g. 0031612345678).

national property

national: str

National trunk form (e.g. 0612345678); international form for a foreign number.

cc property

cc: Optional[str]

Country code, if it matched the configured home country.

nsn property

nsn: str

National significant number (digits after the country code).

format

format(fmt: str) -> str

Format into "e164" | "plain" | "international" | "national".

rewrite_identities()

Both the proxy request and the B2BUA call expose rewrite_identities(policy=None, format=None, headers=None, home=None), which walks the identity headers and reformats each dialable userpart in place, returning the number of headers changed. Pass either a named policy from number_policies: or an inline format with an optional headers list and home override. See request.rewrite_identities and call.rewrite_identities for the full signatures and examples.

On the B2BUA, call.dial(number_policy="<name>") / call.fork(number_policy=…) (or the b2bua.default_number_policy config default) normalize the A-leg identity headers that flow to the B-leg plus the dial/fork target as the final step before the INVITE is built.