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:
- Common Name or SAN must be the SBC FQDN. A wildcard such as
*.contoso.commatchessbc.contoso.combut, per RFC 2818, does not matchsbc.test.contoso.com. - The issuing CA must be in the Microsoft Trusted Root Certificate Program. Self-signed or private-PKI certificates are rejected at the TLS handshake. Use a public CA (DigiCert, Sectigo, GlobalSign, and so on).
- EKU must include Server Authentication. Most CAs require a private key of at least 2048 bits.
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/14and52.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 ports3478-3481and49152-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:
SendSipOptions $true(the default, stated explicitly): the SBC must answer SIP OPTIONS pings or Microsoft excludes it from health/alerting and you fly blind in the SBC health dashboard.MaxConcurrentSessions 500: at 90% of this number the tenant is alerted. Set it to licensed trunk capacity – it arms monitoring, it does not enforce a cap.ForwardPai $true: forwards P-Asserted-Identity so the carrier can bill/CLID even when the caller presents as Anonymous.ForwardCallHistory $true: sendsHistory-InfoandReferred-Byso the carrier sees the diversion on forwarded/transferred calls.FailoverTimeSeconds 10: if the SBC does not respond in this window, the call tries the next trunk. Raise it only on slow networks; too high and a dead SBC holds calls hostage.
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:
- Self-diagnostics: in the Microsoft 365 admin center, run the Direct Routing user diagnostic against the test user – it checks gateway pairing, policy assignment, and licensing in one pass. (Not available in GCC High, DoD, or 21Vianet.)
- Call Analytics (Teams admin center > Users > Call history): inspect a real call leg by leg – codec, jitter, packet loss, egress SBC. First stop when “calls connect but audio is one-way,” which usually means media ports or NAT on the SBC.
- Call Quality Dashboard (CQD): the tenant-wide aggregate. Use it to spot a single SBC site degrading over time; Call Analytics is per-call, CQD is the trend.
- Place real test calls, including an emergency test via the
933mask, and confirm the civic address reaching the provider matches the caller’s location.
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.