Skip to content

Namespace Linkages

A namespace linkage is your namespace’s declared relationship with one peer namespace. Linkages live under the top-level ns-linkages: key, keyed by peer, and each entry has two independent halves:

  1. The transfer seam (call time) — whether the peer may hand calls into your namespace, and the translation, caller-ID, and STIR/SHAKEN policy applied when a call crosses the boundary in either direction.
  2. The inheritance grant (configuration time) — meaningful only when the peer is your direct child: which namespace profile it inherits, which aliases it is granted, and the limits you impose on it.
ns-linkages:
child: # applies to every direct child
allow-inbound: true
inbound-transfer-context: from-tenants
inheritance:
profile: standard-tenant
0197f9d2-...-8c1a: # a specific child — overrides "child" for it
allow-inbound: true
inbound-transfer-context: from-tenants
caller-id-mode: provided
allowed-caller-ids: ["+1415555....", "+1415556...."]
inheritance:
profile: premium-tenant
max-concurrent-calls: 100

The most useful mental model: a linkage is a virtual trunk. Your namespace treats a call arriving from a peer exactly the way it treats a call arriving on a SIP trunk — it lands in a translation context you chose, its asserted caller-ID is screened under a trust mode you chose, and your STIR/SHAKEN ingress policy decides what its signature is worth. The peer’s internal structure is irrelevant, just as a carrier’s is.

An entry’s key names the peer it applies to. Four forms are accepted:

Key Applies to
a namespace ID exactly that peer
a received alias (e.g. pstn) the namespace the alias resolves to
child every direct child, unless a specific entry exists for it
parent your direct parent

At call time the most specific entry wins, in this order: exact-nsid key → alias key resolving to the peer → child/parent shorthand. The winning entry is used in its entirety — a specific entry does not blend with the shorthand entry, so a per-child entry must restate everything it needs.

Each namespace controls its own boundary, and only its own:

  • Entry is gated by the receiver. When a call is transferred from namespace A to namespace B, the switch consults B’s linkage keyed by A. If no entry matches, or the matching entry does not set allow-inbound: true, the transfer is refused — the caller receives the route target’s failure treatment (reorder by default, a fast-busy / SIP 503).
  • allow-inbound defaults to deny. An unconfigured peer cannot enter, and that includes your own children and your own parent. The classic symptom is a child whose on-net calls all work while every off-net call fast-busies: the parent has not yet allowed inbound transfers from that child, so the child’s pstn hops are refused at the parent’s door.
  • Exit needs no permission from your side. There is no “allow-outbound” — if a route target resolves to a peer namespace, the call is offered to it, and the peer’s ACL decides. Your linkage’s outbound fields shape the call on the way out; they do not gate it.
  • Admission caps apply at the door. A transfer entering a namespace is checked against that namespace’s concurrent-call cap (and its partition’s) before it is accepted; over-limit transfers receive the all-circuits-busy treatment.

Calls reach a peer namespace through three route shapes, all subject to the same model: a namespace target (nsid or granted alias), the pstn target (shorthand for namespace with the granted alias literally named pstn), and an alias-namespace pointer line (a line that transfers instead of ringing devices — how a parent delivers DIDs down into a child).

A single call may chain through at most 8 namespace transfers; deeper chains are treated as a loop and released.

When a call in namespace A resolves to a target in namespace B, the hop proceeds in order:

  1. ACLB’s linkage keyed by A is resolved; without allow-inbound: true the transfer ends here.
  2. A’s outbound shaping (from A’s linkage keyed by B, if any):
    • outbound-transfer-context — a translation context run over the dialed number for rewrites only; any target it resolves is ignored (the destination is the transfer itself).
    • outbound-caller-id-context — a caller-ID context correcting the presented external caller-ID before hand-off.
    • transmit-passport — folded into the call’s carried egress policy (most restrictive wins), constraining what the eventual trunk hop may emit.
  3. AdmissionB’s namespace/partition concurrent-call caps.
  4. B’s ingress caller-ID screen — the peer’s asserted external caller-ID is screened exactly like trunk ingress, in the order caller-id-modeinbound-caller-id-contextallowed-caller-ids (details below). A violation is released with the invalid-caller-id treatment.
  5. Presentation — if B’s linkage sets present-external-caller-id, the screened external caller-ID becomes what B’s destination sees; otherwise the original internal caller-ID is preserved across the boundary.
  6. B’s STIR/SHAKEN ingress — the linkage’s attestation-mode and resign-policy replace whatever the call carried (see below), inbound verification re-runs under B’s policy, and passthrough-block rejects an unsigned arrival.
  7. B’s inbound walk — the call enters B’s inbound-transfer-context and routes under B’s configuration from there.

The ingress fields mirror the trunk-profile caller-ID model exactly, applied to the external caller-ID the peer asserts (across a namespace boundary, internal extension identity is meaningless — a child presenting a DID to its parent is treated like a PSTN trunk presenting one):

Field Direction Meaning
caller-id-mode ingress Trust level: fixed, provided (default), or modify
primary-caller-id ingress The number fixed mode stamps (required with fixed)
inbound-caller-id-context ingress Rewrite context run under modify mode only
allowed-caller-ids ingress Allowlist screened after mode/context; empty = unrestricted
present-external-caller-id presentation Show the (screened) external CID to the destination, instead of preserving the internal CID
outbound-caller-id-context egress Rewrite applied when you transfer a call out to this peer

Typical choices: a parent trusts a well-behaved child with provided + an allowed-caller-ids list pinning the child to its own DIDs; fixed + primary-caller-id forces everything from a peer to one billing number; modify normalizes a peer’s formatting before the allowlist screen.

present-external-caller-id is the knob that makes PSTN delivery look right: when the parent hands an inbound PSTN call down into a child, setting it on the child’s parent linkage makes the child’s phones display the real outside caller rather than the parent’s internal identity. Leave it unset for PBX-to-PBX handoffs where extensions should keep seeing each other’s internal numbers.

A linkage carries the same three knobs as a trunk profile, with the same directional split — see Trunk Profiles for the full semantics:

  • attestation-mode (ingress) — the attestation level your namespace asserts if it signs a call that entered over this linkage. Required on every linkage once your namespace enables outbound signing.
  • resign-policy (ingress) — the disposition of the signature the call arrived with (passthrough, passthrough-block, resign, keep-sign; default keep-sign).
  • transmit-passport (egress) — which STIR/SHAKEN headers survive when you transfer a call out over this linkage (transmit-full, transmit-verstat, strip).

The crossing itself is a hard trust boundary: whatever attestation intent the call carried inside the peer is discarded at entry and replaced by your linkage’s values. A child can never dictate the attestation level its parent signs with, and an unsigned call never inherits a signed appearance by crossing a boundary.

The inheritance: block is the configuration-time half, read by the system when a direct child’s configuration is compiled. It is only meaningful on an entry keyed by a child’s nsid or on the child shorthand — an inheritance: block on a parent-keyed or alias-keyed entry is ignored. An nsid-keyed entry carrying a grant must name an actual direct child; anything else (a grandchild, a sibling, an arbitrary namespace ID) is rejected at validation.

Field Effect on the child
profile The namespace profile whose selections merge into the child
granted-aliases Alias → namespace grants, overlaying the profile’s set. A value is self (you), a namespace UUID, or the name of an alias you received (re-granting it by name) — anything else is rejected at validation
max-concurrent-calls A cap on the child’s concurrent calls (0 = none imposed)
permitted-edge-ips Restricts the edge-proxy values the child may configure — exact IPs/hosts or CIDR blocks
permitted-sip-realms Restricts the child’s sip-realm — each entry permits the realm and its subdomains

The imposed values are stamped onto the child outside its own document, so the child cannot loosen them in YAML:

  • The child’s enforced call cap is min(child's own max-concurrent-calls, your imposed cap) — a child can restrict itself further, never raise your limit. (Your partition’s account-wide cap still applies above both.)
  • The permitted-* lists compose with the partition’s own allow-lists: the child’s chosen value must satisfy every list present.
  • Restrictions flow down, and a grant can only narrow them. When a grant omits permitted-edge-ips / permitted-sip-realms, the child is held to the same permitted lists you are held to. Setting a list explicitly replaces that default for the child, but every entry must fall within your own permitted envelope — the intersection of your partition’s allow-lists and what your own parent permits you. A granted CIDR must lie inside one of your permitted CIDRs, a granted realm inside one of your permitted realms; an entry outside your envelope is rejected when you store the version. No namespace can hand a child endpoints it cannot use itself.
  • List entries are also checked for well-formedness (a malformed CIDR, an empty entry) when you store the version, so a bad entry is rejected at its source rather than mysteriously rejecting the child’s edge-proxy later.

The parent accepts calls from every tenant, delivers a DID into one child, and imposes limits:

# ---- parent (carrier / reseller) ----
translations:
from-tenants:
# Tenants hand up E.164; route it out the wholesale route.
"100":
match:
- regex: "^\\+"
target:
resource-type: route
resource-id: wholesale-out
failure-treatment: temporary-failure
"900":
match:
- always: true
target:
resource-type: treatment
resource-id: cannot-complete-as-dialed
from-carrier:
# DID block owned by tenant1 → pointer line → transfers down.
"100":
match:
- exact: "+14155551000"
target:
resource-type: line
resource-id: tenant1-main
lines:
tenant1-main:
alias-namespace: 0197f9d2-aaaa-bbbb-cccc-0123456789ab # tenant1's nsid
ns-linkages:
child:
allow-inbound: true
inbound-transfer-context: from-tenants
caller-id-mode: provided
inheritance:
profile: standard-tenant
0197f9d2-aaaa-bbbb-cccc-0123456789ab: # tenant1 gets tighter terms
allow-inbound: true
inbound-transfer-context: from-tenants
caller-id-mode: provided
allowed-caller-ids: ["+14155551..."] # pinned to their DID block
inheritance:
profile: standard-tenant
max-concurrent-calls: 50
permitted-sip-realms: [tenant1.example.net]

The child allows its parent in (for DID delivery) and presents outside callers honestly:

# ---- tenant1 (child) ----
translations:
from-parent:
"100":
match:
- ast-pattern: "_+1415555....."
modify:
operations:
- isolate-digits: "7:" # +14155551234 → extension 1234
target:
resource-type: line
resource-id: "!dialedNum"
not-found-treatment: unallocated-number
"900":
match:
- always: true
target:
resource-type: treatment
resource-id: unallocated-number
ns-linkages:
parent:
allow-inbound: true
inbound-transfer-context: from-parent
present-external-caller-id: true

Outbound, tenant1’s lines dial resource-type: pstn in their (inherited) translation context; the granted pstn alias resolves to the parent, the parent’s child linkage admits the call into from-tenants, and the allowlist on the nsid-keyed entry holds tenant1 to its own DIDs.

In a managed-authority namespace, linkages are not authored in YAML — they are individual resources created and updated through the /v1/ns/{nsid}/resources/linkages/{peer} REST endpoints, with the same fields and the same semantics. Everything above about keys, permissions, and grants applies unchanged.