Microsoft 365 Email & Collaboration

Deploying Teams Phone with Direct Routing: SBC Pairing, Voice Routing Policies, and Dial Plans

Direct Routing is the only Teams PSTN option where you own the trunk. Microsoft hands you a SIP peering surface in Azure; everything from the certificate on the Session Border Controller (SBC) to the regex on a voice route is yours to get right. That control is the reason to choose it, and also why a misordered PSTN usage or an unsigned certificate quietly drops calls in production. This guide builds the full call path in the order the platform evaluates it: SBC, gateway, routes, usages, policy, number, dial plan, emergency calling.

Everything below assumes the modern MicrosoftTeams module. The legacy Skype for Business Online connector is retired; install and connect once:

Install-Module MicrosoftTeams -Scope CurrentUser
Connect-MicrosoftTeams

The three PSTN options, and where Direct Routing fits

Before touching an SBC, be deliberate about why you are not using a managed option. All three can coexist in one tenant, even on one user.

Option Who runs the trunk Setup surface Best when
Calling Plans Microsoft Buy a license, assign a number Microsoft sells numbers in your country and you want zero telco ops
Operator Connect A participating carrier Carrier provisions through the admin center You want a carrier contract but no SBC to manage
Direct Routing You (or a hosting provider) Pair a certified SBC, author all routing Microsoft has no Calling Plan in-country, you have an existing carrier contract, or you must reach analog/PBX/contact-center gear

The decision comes down to country coverage and existing telephony assets. A depreciating contact center, analog overhead paging, or an active SIP trunk contract all argue for Direct Routing. Mixing is legitimate: a user with a Calling Plan license and a voice routing policy routes matching patterns down the SBC and everything else through the Microsoft Calling Plan, which always applies as the implicit last route.

Step 1 - SBC prerequisites: domains, certificate, FQDN

The single most common pairing failure is a domain that was never registered. The SBC’s FQDN must sit under a domain added to your tenant – you cannot use the *.onmicrosoft.com default, and the domain must be verified before pairing.

# The SBC FQDN domain must be one of these (not *.onmicrosoft.com)
Get-CsTenant | Select-Object -ExpandProperty Domains

If the SBC is sbc01.sip.contoso.com, the domain sip.contoso.com must be verified in the tenant and have at least one licensed user under it. Provisioning can take up to 24 hours to propagate.

The certificate rules are non-negotiable and trip up most first deployments:

The SBC terminates TLS to three Microsoft SIP proxy FQDNs, tried in order: sip.pstnhub.microsoft.com (global, always first), sip2.pstnhub.microsoft.com (secondary), and sip3.pstnhub.microsoft.com (tertiary). Your firewall must permit all three, and inbound SIP can originate from any IP in the subnets below, not only the resolved ones.

Firewall facts to pin down before the network team asks. Signaling and media both use subnets 52.112.0.0/14 and 52.120.0.0/14 (commercial cloud). SIP/TLS from the SBC targets destination port 5061; the SIP proxy replies from source ports 1024-65535. Without media bypass, media (UDP/SRTP) uses media-processor ports 3478-3481 and 49152-53247. GCC High and DoD use different single FQDNs and subnets, and there the SBC must listen on 5061 specifically.

Step 2 - Pair the SBC: create the PSTN gateway

Pairing is a single cmdlet, but the defaults matter. New-CsOnlinePSTNGateway creates the gateway disabled unless you pass -Enabled $true, and the parameter is SipSignalingPort (the old double-l spelling SipSignallingPort was renamed).

New-CsOnlinePSTNGateway `
  -Fqdn sbc01.sip.contoso.com `
  -SipSignalingPort 5061 `
  -Enabled $true `
  -SendSipOptions $true `
  -MaxConcurrentSessions 500 `
  -ForwardCallHistory $true `
  -ForwardPai $true `
  -FailoverTimeSeconds 10 `
  -MediaBypass $false `
  -Description "Primary SBC - DC1 carrier trunk"

What each non-default choice buys you:

To drain an SBC for maintenance, set it disabled – existing calls survive, new calls route elsewhere:

Set-CsOnlinePSTNGateway -Identity sbc01.sip.contoso.com -Enabled $false

Step 3 - Validate the pairing and SIP health

Confirm the gateway is present and enabled before building anything on top of it. The output should show your FQDN with Enabled : True.

Get-CsOnlinePSTNGateway | Format-List Identity, Enabled, SipSignalingPort, MediaBypass, MaxConcurrentSessions

Health monitoring rides on SIP OPTIONS. With SendSipOptions $true, the SBC health dashboard in the Teams admin center (Voice > Direct Routing) shows TLS connectivity, SIP OPTIONS status, and concurrent-call counts per gateway. If a freshly paired SBC shows no OPTIONS traffic, the cause is almost always the certificate chain, a firewall blocking 5061 inbound, or the SBC’s TLS context not trusting Microsoft’s root CAs. The dashboard lags real time by a few hours.

Step 4 - PSTN usages, voice routes, and voice routing policies

The layering is precise. PSTN usages are named containers; voice routes match a dialed-number regex and point at gateways; voice routing policies are an ordered list of usages assigned to users. Build bottom-up.

4a. Create PSTN usages

Usages are strings on the global object. Order inside a policy is what matters later, so name them by scope.

Set-CsOnlinePstnUsage -Identity Global -Usage @{Add="US and Canada"}
Set-CsOnlinePstnUsage -Identity Global -Usage @{Add="International"}

# Confirm (the list can be truncated; expand it):
(Get-CsOnlinePstnUsage).Usage

4b. Create voice routes

A route ties a NumberPattern (a .NET regex against the called number in E.164) to a OnlinePstnGatewayList. Lower Priority wins; SBCs within a route are tried in random order. Build an active route, a backup at lower priority, and a catch-all.

# Active route for Seattle area codes -> primary SBCs
New-CsOnlineVoiceRoute -Identity "Redmond 1" `
  -NumberPattern "^\+1(425|206)(\d{7})$" `
  -OnlinePstnGatewayList sbc01.sip.contoso.com, sbc02.sip.contoso.com `
  -Priority 1 -OnlinePstnUsages "US and Canada"

# Backup route, same pattern, secondary SBCs
New-CsOnlineVoiceRoute -Identity "Redmond 2" `
  -NumberPattern "^\+1(425|206)(\d{7})$" `
  -OnlinePstnGatewayList sbc03.sip.contoso.com, sbc04.sip.contoso.com `
  -Priority 2 -OnlinePstnUsages "US and Canada"

# Catch-all for the rest of US/Canada
New-CsOnlineVoiceRoute -Identity "Other +1" `
  -NumberPattern "^\+1(\d{10})$" `
  -OnlinePstnGatewayList sbc01.sip.contoso.com, sbc02.sip.contoso.com `
  -OnlinePstnUsages "US and Canada"

# Everything else (international), its own usage
New-CsOnlineVoiceRoute -Identity "International" `
  -NumberPattern ".*" `
  -OnlinePstnGatewayList sbc01.sip.contoso.com, sbc02.sip.contoso.com `
  -OnlinePstnUsages "International"

Two failure modes to internalize. An invalid regex in NumberPattern silently fails to match – test patterns against real numbers first. And the extension is stripped before matching: +14255550100;ext=123 matches as +14255550100.

4c. Create and order voice routing policies

The policy is an ordered list of usages. Usages are evaluated top to bottom and the first match wins – later usages are never consulted. Put the most specific usage first.

# US-only users: one usage
New-CsOnlineVoiceRoutingPolicy "US Only" -OnlinePstnUsages "US and Canada"

# Unrestricted users: US/Canada FIRST so Redmond special-handling applies,
# then International as the fallthrough.
New-CsOnlineVoiceRoutingPolicy "No Restrictions" -OnlinePstnUsages "US and Canada", "International"

Never edit the global (Org-wide default) voice routing policy unless you intend every voice-enabled user – including Calling Plan and Operator Connect users – to inherit it. Doing so can silently divert their PSTN calls down your SBC. Always assign custom policies to the users who should use Direct Routing.

To reorder usages in an existing policy, replace the whole ordered set:

Set-CsOnlineVoiceRoutingPolicy -Identity "No Restrictions" `
  -OnlinePstnUsages @{Replace="US and Canada","International"}

Step 5 - Assign number, Enterprise Voice, and the voice routing policy

A user needs three things to place a Direct Routing call: a number, Enterprise Voice enabled, and a voice routing policy. Set-CsPhoneNumberAssignment -PhoneNumberType DirectRouting does the first two at once – assigning a number automatically flips EnterpriseVoiceEnabled to true.

# Assign a Direct Routing DID; EnterpriseVoiceEnabled becomes True automatically
Set-CsPhoneNumberAssignment `
  -Identity user1@contoso.com `
  -PhoneNumber "+14255550123" `
  -PhoneNumberType DirectRouting

# Assign the voice routing policy
Grant-CsOnlineVoiceRoutingPolicy -Identity user1@contoso.com -PolicyName "US Only"

If you need to enable Enterprise Voice without (yet) assigning a number – for example a resource account – use the attribute parameter set, which is mutually exclusive with -PhoneNumber:

Set-CsPhoneNumberAssignment -Identity cafe-aa@contoso.com -EnterpriseVoiceEnabled $true

Direct Routing numbers support extensions natively, which is how you map a single trunk DID to a PBX-style range:

Set-CsPhoneNumberAssignment -Identity user2@contoso.com `
  -PhoneNumber "+14255550100;ext=1234" -PhoneNumberType DirectRouting

Step 6 - Tenant dial plans: normalization rules

A dial plan converts what a human dials (a 5-digit extension, a 7-digit local number, a 9 for an outside line) into E.164 so a voice route can match it. Normalization rules are ordered Pattern/Translation pairs; Teams walks them top-down and stops at the first match. Build them in memory, then attach to a dial plan.

# 5-digit internal extension dialing -> full E.164
$ext = New-CsVoiceNormalizationRule -Parent Global `
  -Description "5-digit internal extension" `
  -Pattern '^(\d{5})$' -Translation '+1425555$1' `
  -Name "Internal5" -IsInternalExtension $true -InMemory

# Local 7-digit dialing in the 425 area -> E.164
$local = New-CsVoiceNormalizationRule -Parent Global `
  -Description "Local 7-digit" `
  -Pattern '^(\d{7})$' -Translation '+1425$1' `
  -Name "Local7" -InMemory

# National: a leading 9 then 1+10 digits -> strip the 9, add +
$national = New-CsVoiceNormalizationRule -Parent Global `
  -Description "Outside line + national" `
  -Pattern '^9(1\d{10})$' -Translation '+$1' `
  -Name "Outside9National" -InMemory

New-CsTenantDialPlan -Identity "Seattle-HQ" `
  -Description "Seattle HQ dial plan" `
  -SimpleName "Seattle-HQ" `
  -NormalizationRules @($ext, $local, $national)

Grant-CsTenantDialPlan -Identity user1@contoso.com -PolicyName "Seattle-HQ"

Two habits. Always normalize to a leading +. A rule that emits a number without + triggers a second normalization pass against tenant/regional rules – double normalization with surprising results. If the carrier needs + removed on the wire, do it at the trunk with OutboundPstnNumberTranslationRules on the gateway, not in the dial plan. Order restrictive rules above permissive ones – a .* rule before ^(\d{5})$ would swallow extensions.

Step 7 - Emergency calling and E911 routing

Emergency calling earns its operational scrutiny. Two policy types do different jobs: emergency call routing decides which numbers are emergency numbers and which trunk they take; emergency calling decides who gets notified. For dynamic location, you wire the Location Information Service (LIS) to network identifiers.

Create an emergency number object and bind it to a routing policy. The dial mask lets users dial a test string (933) that the system treats as 911. The OnlinePSTNUsage must already exist and point at the route to your emergency-capable trunk.

# Emergency number: dial 911 (or 933 as a test mask) out the USE911 usage
$en = New-CsTeamsEmergencyNumber -EmergencyDialString "911" `
  -EmergencyDialMask "933" -OnlinePSTNUsage "USE911"

New-CsTeamsEmergencyCallRoutingPolicy -Identity "US-E911" `
  -EmergencyNumbers @{add=$en} `
  -AllowEnhancedEmergencyServices:$true `
  -Description "US dynamic E911"

Grant-CsTeamsEmergencyCallRoutingPolicy -Identity user1@contoso.com -PolicyName "US-E911"

For dynamic (location-aware) E911, the Teams client reports its network position and LIS returns a civic address. Associate emergency locations with network identifiers – subnet is the most common:

# Map an internal subnet to a previously created emergency location
$loc = Get-CsOnlineLisLocation -City Seattle
Set-CsOnlineLisSubnet -Subnet 10.20.30.0 -LocationId $loc.LocationId `
  -Description "Seattle HQ Floor 3 - 10.20.30.0/24"

Two Direct-Routing-specific requirements. Set -PidfLoSupported $true on the emergency-egress gateway so Teams sends the PIDF-LO XML location payload to the SBC, which relays it to the emergency service provider:

Set-CsOnlinePSTNGateway -Identity sbc01.sip.contoso.com -PidfLoSupported $true

Second, for users who never move you can attach a static location to a DID by passing -LocationId to Set-CsPhoneNumberAssignment. Dynamic LIS lookups override the static value when a network match exists.

Verify

Validate the path from policy down to a placed call – do not wait for a user to report a dropped 911 attempt.

# 1. Gateway is enabled and healthy
Get-CsOnlinePSTNGateway | Format-List Identity, Enabled, SipSignalingPort, PidfLoSupported

# 2. Routes resolve in the priority you expect
Get-CsOnlineVoiceRoute | Sort-Object Priority |
  Format-Table Identity, Priority, NumberPattern, OnlinePstnUsages, OnlinePstnGatewayList

# 3. The user has number, Enterprise Voice, and the right policy
Get-CsOnlineUser user1@contoso.com |
  Format-List UserPrincipalName, LineUri, EnterpriseVoiceEnabled, OnlineVoiceRoutingPolicy, TenantDialPlan

# 4. Dial plan normalizes a dialed string the way you intend
Test-CsEffectiveTenantDialPlan -DialedNumber 42555 -Identity user1@contoso.com

Then go beyond cmdlets:

Enterprise scenario

A multinational with 9,000 seats consolidated six regional PBXs onto Teams Phone. North America moved to Calling Plans cleanly, but their EMEA contact center sat behind a Genesys platform reachable only over an existing SIP trunk, and the carrier contract had three years left. They chose Direct Routing for EMEA and hit a sharp constraint: contact-center agents must reach internal Genesys queues and the external PSTN, but compliance required that all PSTN calls keep the trunk inside the corporate network – no traffic could traverse the public internet to a Microsoft media processor and back.

Two design decisions solved it. First, they kept Calling Plan and Direct Routing on the same agent users: a voice routing policy whose most-specific usage matched the internal Genesys number ranges routed those calls down the SBC (staying on-net), while everything else fell through to the Calling Plan. Because the SBC pattern was evaluated first and the Calling Plan applies as the implicit last route, no extra “international” route was needed for agents. Second, to keep PSTN media on-net, they enabled media bypass so RTP flowed directly between the Teams client and the SBC rather than through Microsoft’s media processors.

The ordering of usages was the make-or-break detail. Putting the Genesys usage first guaranteed on-net handling for queue calls:

# Genesys internal ranges FIRST -> on-net via SBC; PSTN falls through to Calling Plan
New-CsOnlineVoiceRoute -Identity "Genesys-Queues" `
  -NumberPattern "^\+44(20)(7\d{6})$" `
  -OnlinePstnGatewayList sbc-emea01.sip.contoso.com `
  -Priority 1 -OnlinePstnUsages "EMEA-Internal"

New-CsOnlineVoiceRoutingPolicy "EMEA-Agents" -OnlinePstnUsages "EMEA-Internal"

The lesson the platform team wrote into their runbook: with mixed connectivity, voice routing policy order is the control plane. A single transposed usage would have sent queue calls out to the PSTN and back, breaking both the compliance boundary and the cost model.

Deployment checklist

Teams PhoneDirect RoutingSBCVoice RoutingPSTN

Comments

Keep Reading