Microsoft Teams for Incredible PBX®

Licensed for use pursuant to GPLv3

Portions Copyright © 2021, Ward Mundy & Associates LLC



Microsoft Setup

Getting Started

Add the Domain

Add the "Activation User"

Add the SBC, Voice Route, Dial Plan, and Voice Routing Policy

PBX Setup

Patch Asterisk


Setup domain (FQDN) and certificate




Transport Settings

Trunk Settings

Allow the Microsoft Teams SIP proxies through the Incredible PBX firewall

Trunk/SBC Verification

Add the [from-teams] context

Setting up Teams PBX extensions


Microsoft configuration

Create and license a user

Assign the phone number

Test Calling


Appendix 1: VitalPBX patch to PJSIP to support Microsoft Teams signaling

Appendix 2: Alternate configuration



This guide will walk through the steps for setting up Microsoft Teams users as extensions to Incredible PBX. The guide consists of three major sections:

At the end of the tutorial, you will be able to use a Teams client (desktop, mobile, or both at once) as an extension to Incredible PBX that functions almost identically to a SIP phone extension.

Microsoft Setup

Getting Started

Sign up for any Microsoft 365 or Office 365 plan that includes Microsoft Teams. You can get a free trial if you just want to "test the waters."

The Microsoft 365 Business line is targeted to individuals and small businesses. The Office 365 E-series are targeted toward enterprises.

The Office 365 E5 plan includes Phone System licenses, if you decide to go big or just want the trial.

Complete the sign-up process. You will need a non-VoIP number--a mobile phone or traditional landline--to verify that you are not a robot.

Create your first user as part of the sign-up process. The domain name you pick here is used as your sign-on name but will not be used with your PBX. This will be your Global Administrator user for your Microsoft tenant.

After completing setup, you will also need to buy (free trial available) a Microsoft 365 Phone System license.

These steps are all completed from within the Microsoft/Office 365 admin center:

Pick either Microsoft 365 Phone System or Microsoft 365 Phone System free trial. NOTE: If you got Office 365 E5, you already have Phone System licensing and do not need to do this step.

When done, you should find both your MS365/Office365 license(s) and Phone System license(s) listed in the Licenses tab of the admin screen.

Add the Domain

Expand the menu on the left, open Settings and go to Domains. Here we add the domain (FQDN) that will be assigned to the PBX or is already assigned.

Click Add Domain and type the domain (FQDN) of the PBX. We are using "".

Note that you must use a name for which you have DNS control. Dynamic DNS is OK as long as you can also manually add DNS records (see next step).

Use the TXT record method to verify.

Add the DNS record before clicking Verify.

After verification, click Continue.

Deselect the Exchange box. We do not need to add any more records at this time. Click Continue.

Add the "Activation User"

Switch to the Users - Active Users screen. Before adding the SBC, we have to create an "Activation User" which is a Teams-licensed user that has the same domain as the SBC (PBX).

Be sure to pick the PBX's domain name for this user.

This user will never log in, so do not worry about password details.

The user must be assigned a license with Teams (MS 365 / Office 365). TIP: if you are doing this in an established Microsoft tenant, you can temporarily unassign a license from another user and assign it to the Activation User. The license can be unassigned from this user once the SBC setup is done.

Do not assign a Phone System license.

Click Next until finished.

Add the SBC, Voice Route, Dial Plan, and Voice Routing Policy

The next steps are performed in Microsoft Teams administration. Go to and log in with your Global Admin user (the one you created at initial sign-up).

Expand the menu on the left, go to Voice, and Direct Routing.

Click + Add under SBCs.

Specify the details for the SBC. Enter the FQDN (the domain name you just set up), toggle Enabled to On, set the signaling port to 5061, enable SIP Options, enable PAI, and set a reasonable concurrent call capacity. Click Save.

After saving your new SBC, you will be back at the Direct Routing screen and see your SBC listed. After setting up the PBX, we will check back here to make sure the SBC is in a healthy state.

Now go to the Voice Routes tab on this screen and click LocalRoute to edit it.

Change the Dialed number pattern to .*

This pattern accepts any digits, *, or # that are dialed; any dial string will be routed to our PBX.

Click Add SBCs and pick your SBC.

Click Add PSTN Usage, create a new one and name it whatever you like, select it, and save & apply.

Lastly, save the voice route and you are returned to the Direct Routing screen.

Navigate to Dial Plans and edit Global.

Click Add to add a Normalization Rule. Call it "No Modification", use the Advanced options, and enter the regex ^(.*)$. Set the translation to $1 .

The reason for this rule is that by default, Teams will apply transformations related to your E.164 region. We do not want any modifications done on the dialed numbers; they should be passed through, exactly as dialed, to our PBX.

After saving the Normalization Rule, save the Dial Plan.

Navigate to Voice Routing Policies; add a new one. Name it MyVoiceRoutingPolicy and click the button to add your PSTN Usage Record, created earlier. Click Save.

PBX Setup

Patch Asterisk

We will use the Asterisk PJSIP code modifications from VitalPBX, released to the public per GPL licensing requirements, to enable Asterisk to communicate with Microsoft's SIP proxies. The patch is included in appendix 1 of this document, originally from this Github commit:

This patch has been tested against Asterisk 18.6.0 and 16.20.0.


To include this patch with the Incredible PBX installer, add after line 214 of (after cd to the source location):

curl | patch -p1

to use the patch directly from Github. Alternatively, save the patch file from appendix 1 and apply the patch with:

patch -p1 < filename.patch

Proceed with the Asterisk build as usual.

An existing Asterisk build can also be rebuilt by applying this patch in the source directory and issuing: make && make install

Setup domain (FQDN) and certificate


Configure the DNS A record for your PBX if you have not done so already. We are using as the domain name configured earlier for Teams and which will point to our PBX. → A → x.x.x.x


In Incredible PBX, go to Admin → Certificate Management:


Transport Settings

In Asterisk SIP Settings → General SIP Settings, click Detect Network Address to make sure FreePBX knows about its external IP.

In the SIP Settings [chan_pjsip] tab, pick the certificate you just generated, set SSL Method to TLSv1.2, and set Verify Client and Verify Server to Yes. Set the TLS transport to Yes and click Submit.

Go back into the SIP Settings [chan_pjsip] tab and scroll to the bottom; confirm that the TLS Port to Listen On is set as 5061.

Click the Apply Config button at the top. Don't restart Asterisk yet.

Edit /etc/asterisk/pjsip.transports_custom_post.conf to add the MS Teams domain parameter, substituting in your FQDN:


Now restart Asterisk with fwconsole restart to bring up the new PJSIP TLS transport with Microsoft signaling capability.

Verification: check the PJSIP TLS transport on the asterisk console

# asterisk -r

mypbx-team*CLI> pjsip show transport

Transport:  <TransportId........>  <Type>  <cos>  <tos>  <BindAddress....................>


Transport:               tls      3     96

 ParameterName              : ParameterValue


 allow_reload               : true

 async_operations           : 1

 bind                       :

 ca_list_file               : /etc/ssl/certs/ca-certificates.crt

 ca_list_path               :

 cert_file                  : /etc/asterisk/keys/

 cipher                     :

 cos                        : 3

 domain                     :

 external_media_address     :

 external_signaling_address :

 external_signaling_port    : 0

 local_net                  :

 method                     : tlsv1_2

 ms_signaling_address       :

 password                   :

 priv_key_file              : /etc/asterisk/keys/

 protocol                   : tls

 require_client_cert        : No

 symmetric_transport        : false

 tos                        : 96

 verify_client              : Yes

 verify_server              : Yes

 websocket_write_timeout    : 100

Trunk Settings

Go to Connectivity → Trunks. Add a PJSIP Trunk.

In the General tab, name the trunk "msteams" and then move to the PJSIP Settings tab.

Apply the following settings:

  • Authentication: None
  • Registration: None
  • SIP Server:
  • SIP Server Port: 5061
  • Context: from-teams
  • Transport:

Move to the Advanced tab and adjust the following (the rest can remain default):

Move to the Codecs tab and make sure at least ulaw is selected.

Save the trunk and Apply Config.

Allow the Microsoft Teams SIP proxies through the Incredible PBX firewall

Use add-fqdn to allow the Microsoft SIP proxies to send us traffic:

./add-fqdn teamsproxies

Select "2" for SIP (TCP). This selection covers TLS (a TCP protocol) as well.

Trunk/SBC Verification

Go to Reports → Asterisk Info and scroll down to PJSIP to verify that endpoint "msteams" shows status "Avail":

Go to the Teams administration site (, Voice → Direct Routing and you should now see your SBC showing "Active" in the TLS connectivity status:

Note that it may take a few minutes for this status to change. Also, it may take several hours until you see the SIP Options status change from Warning to Active.

Add the [from-teams] context

We are using a custom context for our Teams trunk that removes the + that comes in on our Teams extensions' caller IDs.

Edit /etc/asterisk/extensions_custom.conf.

At the top or bottom, add the following short dialplan:


; strip + from caller ID coming from Teams

exten => _.,1,Set(CALLERID(num)=${CALLERID(num):1})

same => n,Goto(from-internal,${EXTEN},1)

Save the file and inform Asterisk of the change with a dialplan reload command:

asterisk -rx "dialplan reload"

Setting up Teams PBX extensions


Go to Applications → Extensions to add a new extension.

Add a new Custom Extension:

Assign an extension number and name as usual:

Configure the Voicemail and FM/FM tabs as you like. In the Advanced tab, set the Dial field to PJSIP/extensionnumber@msteams. For extension 710, it would look like this:

Save the extension and Apply Config.

Microsoft configuration

Create and license a user

Create the user in your Microsoft tenant, or license an existing user with a Teams license (any MS 365 / Office 365) and a Phone System license. Your users do not have to have the same domain name as the SBC/PBX domain; they only have to be within the same tenant/organization. The new user is added from the admin console at

Click Next until finished.

Assign the phone number

After the user is assigned a Phone System license, he needs to have "Enterprise Voice" enabled and the extension number assigned. This is done in Power Shell.

You can use Power Shell from a Windows computer if you download the Microsoft Teams Power Shell connector. (See A more convenient way to access Power Shell is through the Azure portal on the web.

Go to and log in with your Global Administrator account.

Click the >_ symbol to the right of the search bar to bring up Cloud Shell, and then choose Power Shell.

If you don't have an Azure subscription, you will be directed to the following page where you can click Start Free. Follow the prompts to get through verification and free account signup.

If prompted to create a storage location, accept this (free during first month with credit; a penny or two per month afterward).

Expand the Power Shell window.

Import the Microsoft Teams module and log in: (the Power Shell prompt will be represented by PS> ; issue the commands as follows)

PS> import-module MicrosoftTeams

PS> Connect-MicrosoftTeams -usedeviceauthentication

Follow the one-line instruction that follows in order to log in using a separate browser tab. Proceed through the prompts until it says "You have signed in…" We use the "device authentication" method because it works with 2FA logins, should you decide to enable them.

Back in Power Shell, you will see your account ID listed and be returned to the prompt.

Issue the following command to enable your user, replacing the identity field with your user's login ID and the onpremlineuri field with your user's PBX extension (set up in the previous section), in the format of tel:extensionnumber:

PS> set-csuser -identity "" -enterprisevoiceenabled $true -hostedvoicemail $false -onpremlineuri tel:710

You may receive an error like: Set-CsUser: Management object not found for identity "". If so, wait a little while and try again. There can be propagation delays with the licensing.

Grant the user the Voice Routing Policy you set up previously by issuing the Grant-CsOnlineVoiceRoutingPolicy command:

PS> Grant-CsOnlineVoiceRoutingPolicy -identity "" -policyname "MyVoiceRoutingPolicy"

You are ready to test.

Test Calling

Install Microsoft Teams on desktop or mobile if you don't already have it and then log in. You can also use Teams on the web by navigating to Use a private/incognito window so that you can log in with a different account than your tenant's Global Administrator.

Navigate to the Calls tab. You should see a dial pad and your extension number. If you see a screen without the dial pad, the Microsoft provisioning is still in progress.

The dial pad is not yet shown

Wait 30 minutes or so and restart Teams. Microsoft notes that it may take up to 4 hours after Enterprise Voice has been enabled and the Voice Routing Policy has been applied for the dial pad to appear.

Teams with dial pad on desktop

Teams mobile with dial pad


Use the dial pad to call other PBX extensions, PSTN numbers, or internal feature codes such as *97. Call to your Teams extension from a SIP phone. If you have both a desktop and a mobile client logged in to Teams, both will ring on an incoming call.

Your Teams extension can be added to ring groups, targeted directly by an Inbound Route, and generally participate in Incredible PBX functions.

Note: To add a Teams extension to a ring group, treat it as if you were adding an external number; e.g. add it to the ring group as 710#.


Appendix 1: VitalPBX patch to PJSIP to support Microsoft Teams signaling

From 203b6ebb9b976a496e524fc41b3b4f2b7480fff1 Mon Sep 17 00:00:00 2001

From: Jose Rivera <>

Date: Mon, 17 May 2021 14:23:37 -0600

Subject: [PATCH] Add support for MS Team implementation

The variable ms_signaling address is now added. This variable is not stringify nor change from domain to IP.


 include/asterisk/res_pjsip.h     |  2 ++

 res/res_pjsip.c                  |  6 ++++++

 res/res_pjsip/config_transport.c | 32 ++++++++++++++++++++++++++++++++

 res/res_pjsip_nat.c              | 19 +++++++++++++++++--

 4 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h

index 2020ca8782c..6f155f893af 100644

--- a/include/asterisk/res_pjsip.h

+++ b/include/asterisk/res_pjsip.h

@@ -188,6 +188,8 @@ struct ast_sip_transport {


                 /*! Optional domain to use for messages if provided could not be found */


+        /*! MS Team Variable */

+        AST_STRING_FIELD(ms_signaling_address);


         /*! Type of transport */

         enum ast_transport type;

diff --git a/res/res_pjsip.c b/res/res_pjsip.c

index 775b63f8d1a..b0f2f2268fa 100644

--- a/res/res_pjsip.c

+++ b/res/res_pjsip.c

@@ -1688,6 +1688,9 @@

                                 <configOption name="external_signaling_address">

                                         <synopsis>External address for SIP signalling</synopsis>


+                                <configOption name="ms_signaling_address">

+                    <synopsis>MS Team address for SIP signalling</synopsis>

+                </configOption>

                                 <configOption name="external_signaling_port" default="0">

                                         <synopsis>External port for SIP signalling</synopsis>


@@ -2512,6 +2515,9 @@

                                 <parameter name="ExternalSignalingAddress">

                                         <para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_signaling_address']/synopsis/node())"/></para>


+                                <parameter name="MSTeamSignalingAddress">

+                    <para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='ms_signaling_address']/synopsis/node())"/></para>

+                </parameter>

                                 <parameter name="ExternalSignalingPort">

                                         <para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_signaling_port']/synopsis/node())"/></para>


diff --git a/res/res_pjsip/config_transport.c b/res/res_pjsip/config_transport.c

index 830b03832d9..b6a66a1fdaf 100644

--- a/res/res_pjsip/config_transport.c

+++ b/res/res_pjsip/config_transport.c

@@ -953,6 +953,35 @@ static int privkey_file_to_str(const void *obj, const intptr_t *args, char **buf

         return 0;



+/*! \brief Custom handler for ms team parameter */

+static int transport_ms_signaling_address_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)


+        struct ast_sip_transport *transport = obj;

+    RAII_VAR(struct ast_sip_transport_state *, state, find_or_create_temporary_state(transport), ao2_cleanup);


+    if (!state) {

+                    return -1;

+    }


+    if (ast_strlen_zero(var->value)) {

+        /* Ignore empty options */

+        return 0;

+    }


+        ast_string_field_set(transport, ms_signaling_address, var->value);


+    return 0;



+static int transport_ms_signaling_address_to_str(const void *obj, const intptr_t *args, char **buf)


+        const struct ast_sip_transport *transport = obj;


+        *buf = ast_strdup(transport->ms_signaling_address);


+        return 0;



 /*! \brief Custom handler for turning a string protocol into an enum */

 static int transport_protocol_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)


@@ -1650,6 +1679,9 @@ int ast_sip_initialize_sorcery_transport(void)

         ast_sorcery_object_field_register(sorcery, "transport", "domain", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, domain));

         ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_server", "", transport_tls_bool_handler, verify_server_to_str, NULL, 0, 0);

         ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_client", "", transport_tls_bool_handler, verify_client_to_str, NULL, 0, 0);


+        ast_sorcery_object_field_register_custom(sorcery, "transport", "ms_signaling_address", "", transport_ms_signaling_address_handler, transport_ms_signaling_address_to_str, NULL, 0, 0);


         ast_sorcery_object_field_register_custom(sorcery, "transport", "require_client_cert", "", transport_tls_bool_handler, require_client_cert_to_str, NULL, 0, 0);

         ast_sorcery_object_field_register_custom(sorcery, "transport", "method", "", transport_tls_method_handler, tls_method_to_str, NULL, 0, 0);

 #if defined(PJ_HAS_SSL_SOCK) && PJ_HAS_SSL_SOCK != 0

diff --git a/res/res_pjsip_nat.c b/res/res_pjsip_nat.c

index 1b5fdd12603..bbca375aaa8 100644

--- a/res/res_pjsip_nat.c

+++ b/res/res_pjsip_nat.c

@@ -32,6 +32,7 @@

 #include "asterisk/res_pjsip_session.h"

 #include "asterisk/module.h"

 #include "asterisk/acl.h"

+#include "asterisk/strings.h"


 /*! URI parameter for original host/port */

 #define AST_SIP_X_AST_ORIG_HOST "x-ast-orig-host"

@@ -445,7 +446,14 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)

                         pjsip_method_cmp(&cseq->method, &pjsip_register_method)) {

                         /* We can only rewrite the URI when one is present */

                         if (uri || (uri = nat_get_contact_sip_uri(tdata))) {

-                                pj_strdup2(tdata->pool, &uri->host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));


+                                /* Do not stringify the signalling address when using MS Teams */

+                                if (!ast_strlen_zero(transport->ms_signaling_address)) {

+                                        pj_strdup2(tdata->pool, &uri->host, transport->ms_signaling_address);

+                                }else{

+                                        pj_strdup2(tdata->pool, &uri->host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));

+                                }


                                 if (transport->external_signaling_port) {

                                         uri->port = transport->external_signaling_port;

                                         ast_debug(4, "Re-wrote Contact URI port to %d\n", uri->port);

@@ -455,7 +463,14 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)


                 /* Update the via header if relevant */

                 if ((tdata->msg->type == PJSIP_REQUEST_MSG) && (via || (via = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL)))) {

-                        pj_strdup2(tdata->pool, &via->, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));


+                        /* Do not stringify the signalling address when using MS Teams */

+                        if (!ast_strlen_zero(transport->ms_signaling_address)) {

+                pj_strdup2(tdata->pool, &via->, transport->ms_signaling_address);

+            }else{

+                pj_strdup2(tdata->pool, &via->, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));

+            }


                         if (transport->external_signaling_port) {

                                 via->sent_by.port = transport->external_signaling_port;


Appendix 2: Alternate configuration


Teams users can use Microsoft's integrated voicemail service instead of Incredible PBX voicemail. Be sure to enable only one or the other--not both.

In the set-csuser command, change -hostedvoicemail $false to -hostedvoicemail $true. The user then sets up voicemail in the Teams client:

Microsoft voicemail includes speech-to-text transcription and direct integration with Outlook.