{"id":694,"date":"2026-05-10T18:36:17","date_gmt":"2026-05-11T02:36:17","guid":{"rendered":"https:\/\/nramkumar.org\/tech\/?p=694"},"modified":"2026-05-10T18:36:17","modified_gmt":"2026-05-11T02:36:17","slug":"asterisk-sms-inbound-outbound-with-voip-ms-sip-trunk","status":"publish","type":"post","link":"https:\/\/nramkumar.org\/tech\/blog\/2026\/05\/10\/asterisk-sms-inbound-outbound-with-voip-ms-sip-trunk\/","title":{"rendered":"Asterisk SMS Inbound\/Outbound With voip.ms SIP Trunk"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">In this post, we will cover configuring a local Asterisk server to route Inbound and Outbound SMS using a voip.ms trunk. This will also cover routing inbound messages to all known clients at the time the message is received so having multiple clients (like Jami, Linphone) registered to your Asterisk server for the inbound extension will all receive the incoming SMS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture Diagram<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The network architecture diagram is the same as the previous post we had that showed the architecture for the LAN Asterisk server, clients and the voip.ms trunk.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"559\" src=\"https:\/\/nramkumar.org\/tech\/wp-content\/uploads\/2026\/05\/AsteriskNetworkDiagram-1024x559.png\" alt=\"\" class=\"wp-image-690\" srcset=\"https:\/\/nramkumar.org\/tech\/wp-content\/uploads\/2026\/05\/AsteriskNetworkDiagram-1024x559.png 1024w, https:\/\/nramkumar.org\/tech\/wp-content\/uploads\/2026\/05\/AsteriskNetworkDiagram-300x164.png 300w, https:\/\/nramkumar.org\/tech\/wp-content\/uploads\/2026\/05\/AsteriskNetworkDiagram-768x419.png 768w, https:\/\/nramkumar.org\/tech\/wp-content\/uploads\/2026\/05\/AsteriskNetworkDiagram.png 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">voip.ms Setup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before we start on our Asterisk setup, we need to enable SMS on the voip.ms DID. Click DID > Manage DID > Edit. Then enable Message Service (SMS\/MMS) and Link the SMS received to this DID to a SIP Account. Select the sub-account that you have configured as part of your voip.ms trunk in your Asterisk server.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Message Threading<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One of the key issues to address is message threading &#8211; we want messages to be threaded properly by the clients. voip.ms will put the sender information as &lt;number>@&lt;pop server> &#8211; this can be made to work, but a more reliable approach is to normalize all addresses as &lt;number>@asterisk.lan (where this is the hostname of the LAN  Asterisk server) on inbound\/outbound path and do rewrites as necessary on the Asterisk server. This works better with the clients and they all see the consistent path and information for inbound and outbound messages.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Routing To All Known Endpoints<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We can use <code>PJSIP_DIAL_CONTACTS(&lt;extension>)<\/code> to get all the registered contacts for the extension. Then we can loop through each and send the message to each on inbound path ensuring it is delivered to the registered endpoints for the extension.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Extracting the To Address<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">voip.ms puts the To address (your DID) in <code>X-SMS-To<\/code> header. This can be accessed using the <code>MESSAGE_DATA<\/code> function in Asterisk since it is a custom header.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Inbound SMS: voip.ms \u2192 LAN Clients<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">How voip.ms Delivers Inbound SMS<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">voip.ms sends a SIP MESSAGE to your server&#8217;s IP with the destination DID in a custom header:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>MESSAGE sip:s@&lt;asterisk_lan_address>:5061;transport=TLS SIP\/2.0\nFrom: \"5555555555\" &lt;sip:5555555555@pop_server_address>\nTo: &lt;sip:s@asterisk_lan_address:5061;transport=TLS>\nX-SMS-To: &lt;DID_Number><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two things to note: the <code>Request-URI<\/code> user is <code>s<\/code> (not your DID), and the actual destination DID is in <code>X-SMS-To<\/code>. Your dialplan needs to read <code>X-SMS-To<\/code> to know which number was texted.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Dialplan<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Add to <code>\/etc\/asterisk\/extensions.conf<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;sms-inbound]\nexten => s,1,NoOp(Incoming SMS from ${MESSAGE(from)})\n\n\u00a0; MESSAGE_DATA() reads custom SIP headers \u2014 MESSAGE() is only for core attributes\n\u00a0same => n,Set(TARGET_NUMBER=${MESSAGE_DATA(X-SMS-To)})\n\u00a0same => n,Log(NOTICE, SMS to DID ${TARGET_NUMBER} body: ${MESSAGE(body)})\n\n\u00a0; Extract bare number from '\"&lt;number>\" &lt;sip:&lt;number>@&lt;pop_server>>'\n\u00a0same => n,Set(RAW_FROM=${MESSAGE(from)})\n\u00a0same => n,Set(FROM_NUM=${CUT(CUT(RAW_FROM,@,1),:,2)})\n\n\u00a0; Normalize to asterisk.lan \u2014 threads by number regardless of voip.ms edge server\n\n\u00a0same => n,Set(ACTUAL_FROM=sip:${FROM_NUM}@asterisk.lan)\n\n\u00a0same => n,Log(NOTICE, Normalized sender: ${ACTUAL_FROM})\n\n\u00a0; Set To to the internal extension, not the DID.\n\u00a0; Using the DID here causes clients to adopt it as their own SIP identity,\n\u00a0; which breaks outbound endpoint matching.\n\u00a0same => n,Set(MESSAGE(to)=sip:100@asterisk.lan)\n\n\u00a0; Get all registered contacts for extension 100\n\u00a0same => n,Set(CONTACTS=${PJSIP_DIAL_CONTACTS(100)})\n\u00a0same => n,Set(CONTACT_COUNT=${FIELDQTY(CONTACTS,&amp;)})\n\u00a0same => n,Log(NOTICE, ${CONTACT_COUNT} registered contact(s) for exten 100)\n\u00a0same => n,GotoIf($&#91;${CONTACT_COUNT} = 0]?no_contacts)\n\u00a0same => n,Set(ITER=1)\n\u00a0same => n,While($&#91;${ITER} &lt;= ${CONTACT_COUNT}])\n\u00a0\u00a0same => n,Set(CURRENT_CONTACT=${CUT(CONTACTS,&amp;,${ITER})})\n\u00a0\u00a0; CURRENT_CONTACT = PJSIP\/100\/sip:&lt;client_ip>:&lt;client_port> \u2014 field 3 is the SIP URI\n\u00a0\u00a0same => n,Set(CONTACT_URI=${CUT(CURRENT_CONTACT,\/,3)})\n\u00a0\u00a0same => n,Log(NOTICE, Dispatching to pjsip:100\/${CONTACT_URI} from ${ACTUAL_FROM})\n\u00a0\u00a0same => n,MessageSend(pjsip:100\/${CONTACT_URI},${ACTUAL_FROM})\n\u00a0\u00a0same => n,Log(NOTICE, MessageSend status: ${MESSAGE_SEND_STATUS})\n\u00a0\u00a0same => n,Set(ITER=$&#91;${ITER} + 1])\n\u00a0same => n,EndWhile()\n\u00a0same => n,Hangup()\n\nsame => n(no_contacts),Log(WARNING, SMS from ${FROM_NUM} to DID ${TARGET_NUMBER} dropped \u2014 no registered contacts for exten 100)\nsame => n,Hangup()<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">What the Client Receives<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>MESSAGE sip:100@&lt;client_ip>:&lt;client_port> SIP\/2.0\nFrom: &lt;sip:&lt;number>@asterisk.lan>\nTo: \u00a0 &lt;sip:100@&lt;client_ip>><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The client threads the conversation under <code>sip:&lt;number>@asterisk.lan<\/code>. Every future message from that number \u2014 regardless of voip.ms edge \u2014 matches that thread.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Outbound SMS: LAN Clients \u2192 voip.ms<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Add to <code>\/etc\/asterisk\/extensions.conf<\/code><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Dialplan<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;sms-outbound]\n; Match any 10 or 11 digit number\nexten => _X.,1,NoOp(Outbound SMS from ${MESSAGE(from)} to ${EXTEN})\n\u00a0; Extract internal sender extension from 'sip:100@asterisk.lan'\n\u00a0same => n,Set(RAW_FROM=${MESSAGE(from)})\n\u00a0same => n,Set(SENDER_EXTEN=${CUT(CUT(RAW_FROM,@,1),:,2)})\n\n\u00a0; Set To for the voip.ms trunk \u2014 must use their domain, not asterisk.lan\n\u00a0same => n,Set(MESSAGE(to)=sip:${EXTEN}@&lt;pop_server>)\n\n\u00a0; Trunk identity \u2014 DID at voip.ms domain\n\u00a0same => n,Set(TRUNK_FROM=sip:&lt;DID_NUMBER>@&lt;pop_server>)\n\u00a0same => n,Set(TRUNK_TO=pjsip:voipms\/sip:${EXTEN}@&lt;pop_server>)\n\u00a0same => n,Log(NOTICE, SMS from exten ${SENDER_EXTEN} to ${EXTEN} via ${TRUNK_TO})\n\n\u00a0same => n,MessageSend(${TRUNK_TO},${TRUNK_FROM})\n\n\u00a0same => n,Log(NOTICE, Outbound send status: ${MESSAGE_SEND_STATUS})\n\u00a0same => n,GotoIf($&#91;\"${MESSAGE_SEND_STATUS}\" != \"SUCCESS\"]?failed)\n\u00a0same => n,Hangup()\n\nsame => n(failed),Log(ERROR, Outbound SMS to ${EXTEN} failed: ${MESSAGE_SEND_STATUS})\nsame => n,Hangup()<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">What voip.ms Receives<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>MESSAGE sip:&lt;to_number>@&lt;pop_server> SIP\/2.0\nFrom: &lt;sip:&lt;DID_Number>@&lt;pop_server>>\nTo: \u00a0 &lt;sip:&lt;to_number>@&lt;pop_server>><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring Inbound And Outbound Contexts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For Inbound, we need to add the context to the voip.ms trunk in <code>\/etc\/asterisk\/pjsip.conf<\/code>. Under the voip.ms endpoint section, add <code>message_context=sms-inbound<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For outbound, we need to add the context to our extension&#8217;s configuration. In <code>\/etc\/asterisk\/pjsip_extensions.conf<\/code> (since I am using the <code>#include<\/code> approach to keep local extension configuration separate from trunk configuration), under the extension endpoint section, add <code>message_context=sms-outbound<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Client Specific Quirks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I found linphone adopts the <code>To<\/code> header of the received message as their outbound SIP identity when replying. So if you extract the actual DID number and use it as the <code>To<\/code> address in the inbound message, you will see the same come back as <code>sip:&lt;DID_Number>@&lt;asterisk_lan><\/code> when you reply from the client. This will fail as the <code>&lt;DID_Number><\/code> is not a valid extension. The fix is to simply make the <code>To<\/code> address in inbound messages be <code>sip:&lt;extension>@&lt;asterisk_lan><\/code> &#8211; this works fine for both Jami and linphone. Jami works fine without this as well as it uses the configured client identity for the SIP trunk correctly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing And Debugging<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>asterisk -rvv<\/code>v  and <code>pjsip set logger on<\/code> to see the actual traffic both from voip.ms and from your clients for inbound and outbound messages to understand what is actually being sent\/received.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this post, we will cover configuring a local Asterisk server to route Inbound and Outbound SMS using a voip.ms trunk. This will also cover routing inbound messages to all known clients at the time the message is received so having multiple clients (like Jami, Linphone) registered to your Asterisk server for the inbound extension&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-694","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/posts\/694","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/comments?post=694"}],"version-history":[{"count":1,"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/posts\/694\/revisions"}],"predecessor-version":[{"id":695,"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/posts\/694\/revisions\/695"}],"wp:attachment":[{"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/media?parent=694"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/categories?post=694"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nramkumar.org\/tech\/wp-json\/wp\/v2\/tags?post=694"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}