Dial Plans

Introduction

Dial plans control how calls are processed. Each SIP account can have an outgoing dial plan, which processes the calls it places, and an incoming dial plan, which processes the calls it receives. Both are optional: an account with no outgoing dial plan can only call the other SIP accounts on the server, and one with no incoming dial plan rings every device registered to it.

There are three kinds of dial plan, chosen when the dial plan is created on the Dial Plans page:

  • Simple Wizard: a table of rules edited in the portal, with no script to write. See Simple Wizard.
  • Ruby: a script in a small, Ruby-style language made for routing calls. Most of this page is about it.
  • Json: a list of rules written by hand as JSON. See Json dial plans.

The Ruby editor can check a script's syntax and do a test run of it against a made up call without placing any calls, and its Ruby help panel summarises the language. Every real call's dial plan run is recorded on the Call History page, including anything written with sys.Log and any error with the line it happened on; see Call History.

The Dial Plans and SIP Accounts pages show whether each dial plan is ready to use. An error means it can't run, and every call through it fails until it's fixed. A warning means it runs but something in it does nothing, usually a function kept from the previous SIPSorcery so that old dial plans still work; see Moving a dial plan. On the SIP Accounts page, each account's dial plan name links to the dial plan.

Ruby dial plan functions

See also Limits and Read only dial plans and providers.

Function names are not case sensitive and can be written in Ruby's style as well, so sys.log and sys.set_from_header work. The values a dial plan can read, such as sys.In and req.URI.User, are on the Dial Plan Variables page.

sys.Dial

Usage:

sys.Dial(string dialString)
sys.Dial(string dialString, int ringTimeout)

The most important dial plan function. It places a call to one or more destinations and, when the first one answers, connects it to the caller. The dial string controls the order the destinations are tried in. Its syntax is best shown by an example:

# sys.Dial example.
sys.Dial("123@provider1&456@provider2|789@provider3")
  • Calls are placed to 123 through provider1 and 456 through provider2 at the same time, because & rings destinations together.
  • If neither answers, a call is placed to 789 through provider3, because | tries the next part when everything before it has failed.

As soon as one destination answers, all the others are cancelled and the dial plan ends. If none answers, the dial plan carries on with the next line, so a later line can try something else or send a response:

sys.Dial("paul@local", 20)
sys.Respond(480, "Sorry no one available")

The second parameter, ringTimeout, is the number of seconds each part of the dial string may ring before the next is tried. Without it each part rings for 30 seconds. It can be a number or a variable holding one, such as sys.Dial("${dst}@provider1", ring_secs). The server's own call setup limit still applies to the whole call.

A destination in the dial string can be:

  • One of your SIP accounts: fred@local, or the account's full address such as fred@sipsorcery.com. Every device registered to the account rings.
  • A number through one of your providers: 12345@provider1, where provider1 is the name of one of your SIP provider records. The provider's server and credentials are filled in for you.
  • Any other SIP address: music@iptel.org, or a full URI such as sip:music@iptel.org.

${dst} in a dial string is the number or name that was dialled, and ${dst:n} is the same with its first n characters removed, which is how a dial plan strips a prefix it matched on: sys.Dial("${dst:2}@provider1") dials 234 when *1234 was dialled. ${exten} and ${exten:n}, from the previous SIPSorcery, mean the same.

Three more describe the caller: ${fromname} is their display name, ${fromuriuser} the user part of their address, and ${fromaor} their whole address, such as joe@example.com. Anything the caller didn't send is left empty, and an option left empty is ignored, so [fd=${fromname}] passes on the caller's name when they sent one and does nothing when they didn't. The characters | & [ ] , = are removed from what the caller sent, so a caller can't add a destination or an option to your dial string.

Options can be added after a destination in square brackets, separated by commas:

  • fd=string: the From header display name, which most phones show as the caller's name,
  • fu=string: the From header user,
  • fh=string: the From header host,
  • dt=seconds: wait this long, up to 120 seconds, before ringing this destination, so it rings after the others in its part of the dial string. If everything else in that part fails first, it rings straight away rather than leaving the caller waiting. It's never rung once another destination has answered, or after that part of the dial string's ring time is up,
  • rm=a: if this destination answers with a redirect (a 3xx response), call where it redirects to, as part of the same dial. Without it, or with rm=m, a redirect counts as a failure. Up to three redirects in a row are followed. Your provider's username and password go with a redirect only when it stays on the same server.

Additional examples of sys.Dial are shown below.

# The call sequence will be:
#  - Dial via the provider "Provider_Z",
#  - Set the From header display name on the call to 0987654321.
sys.Dial("${dst}@Provider_Z[fd=0987654321]")
# The call sequence will be:
#  - Dial the number via "Provider_1" and "Provider_2" at the same time,
#  - If both fail (no answer, busy or a timeout), dial it via "Provider_3".
# If either of the first two answers, the other is cancelled and the dial plan ends.
sys.Dial("${dst}@Provider_1 & ${dst}@Provider_2 | ${dst}@Provider_3")
# Call the music on hold server anonymously.
sys.Dial("music@iptel.org[fu=anon,fh=anonymous.invalid]")
# Ring the desk phone straight away, and the mobile through a provider 10 seconds later.
sys.Dial("fred@local&0061400000000@provider1[dt=10]")

# SIPBroker answers with a redirect to the number's real address; rm=a calls it.
sys.Dial("${dst:1}@sipbroker.com[rm=a]")

sys.Respond

Usage:

sys.Respond(int statusCode)
sys.Respond(int statusCode, string reason)
sys.Respond(int statusCode, string reason, string customHeaders)
sys.Respond(int statusCode, string reason, string redirectUri)

Sends a SIP response back to the caller (see RFC 3261 response codes). The statusCode can be a number or a variable holding one, so a dial plan can pass on the status a failed call got. The reason is optional; without it the standard text for the status code is used.

  • A provisional response, 100 to 199, is sent and the dial plan carries on. Only the first ringing or progress response reaches the caller; later ones are shown in the trace.
  • A 2xx response is not sent, and the dial plan carries on. A call is only answered by sys.Dial.
  • A redirection, 300 to 399, sends the caller to the redirectUri, which is required, and ends the dial plan.
  • A failure, 400 to 699, is sent and ends the dial plan.

For anything but a redirection the third parameter adds custom SIP headers to the response, written as "Name: value|Other: value".

# sys.Respond examples.
sys.Respond(183, "Looking you up")                                  # The caller hears progress and the dial plan carries on.
sys.Respond(302, "Moved", "#{req.URI.User}@elsewhere.example")     # The caller is redirected and the call ends.
sys.Respond(404, "Sorry could not find your destination", "X-Reason: unknown") # The caller gets a failure response and the call ends.

A dial plan that reaches its end without a call being answered or a final response sent is rejected with a 500 error, so it's good practice to end with a sys.Respond that says what happened.

sys.Log

Usage:

sys.Log(string logMessage)

Writes a message to the call's dial plan trace, shown on the call's page in Call History and in the editor's test run. It's the way to see what a dial plan did with a call and to troubleshoot it.

# sys.Log example.
sys.Log("Log message from the SIPSorcery dial plan.")

SIP header functions

Usage:

sys.SetFromHeader(string fromName, string fromUser, string fromHost) # Sets the From header for all subsequent calls.
sys.ClearFromHeader()                                                 # Takes back sys.SetFromHeader.
sys.SetCustomSIPHeader(string header, string value)                   # Adds a header to all subsequent calls.
sys.RemoveCustomSIPHeader(string header)                              # Removes a header added earlier.
sys.ClearCustomSIPHeaders()                                           # Removes every header added earlier.
sys.PrintCustomSIPHeaders()                                           # Writes the custom headers set so far to the trace.
sys.SetCustomContent(string contentType, string body)                 # Offers this body on all subsequent calls.
sys.SetCustomContent(string body)                                     # The same, keeping the content type.
sys.ClearCustomBody()                                                 # Offers the caller's body again.

sys.SetFromHeader is the most useful of these, because the SIP From header is what usually becomes the caller ID on a call. Pass nil for any part to leave it as it is. Most SIP providers use the From header to authenticate calls, and changing the fromUser part in particular can make a call fail, so when changing caller ID start with only the fromName and change the fromUser only if that doesn't have the effect you want. The fromHost should rarely need setting. A destination's own fd, fu and fh options take priority over it.

# Set caller ID example.
sys.SetFromHeader("Mr Big", nil, nil)
sys.SetCustomSIPHeader("X-Account-Code", "1234")
sys.Dial("${dst}@provider1")

# If that fails, try another provider with the caller's own caller ID and no account code.
sys.ClearFromHeader()
sys.RemoveCustomSIPHeader("X-Account-Code")
sys.Dial("${dst}@provider2")

sys.SetCustomContent replaces the body of the calls that follow, which for an ordinary call is the SDP describing the caller's audio. SIPSorcery passes that through rather than handling the audio itself, so a replacement body has to be one the far end and the caller can both work with; get it wrong and the call connects with no audio. An empty body, or no content type, means the caller's. Each call that offers a custom body says so in its trace.

sys.Email

Usage:

sys.Email(string to, string subject, string body)
sys.Email(string to, string subject, string body, bool isHtml)

Sends an email to the address in the to parameter, and the dial plan carries on. It only sends to your own email address, the one in your Settings, optionally with a +tag added before the @, such as you+calls@example.com, which is useful for filtering. A dial plan can send up to three emails per call and 30 an hour.

# Send email example.
sys.Email("you@example.com", "Email from Dial Plan", "Hello World!")

# Send HTML email example.
sys.Email("you+calls@example.com", "Email from Dial Plan", "<html><body><b>Hello World!</b></body></html>", true)

sys.SendRequest

Usage:

sys.SendRequest(string method, string destination, string contentType, string body, int waitSeconds)
sys.SendRequest(string method, string destination, string contentType, string body, int waitSeconds, string username, string password)

Sends a SIP request that isn't a call, such as an instant message, and the dial plan carries on. The method can be MESSAGE, OPTIONS or NOTIFY. The destination takes the same forms as a sys.Dial destination. waitSeconds is how long to wait for a response, up to 30; 0 sends it and moves on straight away. A username and password are used to answer an authentication challenge, and need a wait to answer within.

# Send an instant message to your phone about the call.
sys.SendRequest("MESSAGE", "fredphone@local", "text/plain", "Missed call from #{req.Header.From.FromURI.User}", 0)

sys.Callback

Usage:

sys.Callback(string first, string second)
sys.Callback(string first, string second, int delaySeconds)
sys.Callback(string first, string second, int delaySeconds, int ringSecondsFirst, int ringSecondsSecond)
sys.Callback(string first, string second, int delaySeconds, int ringSecondsFirst, int ringSecondsSecond, string headersFirst, string headersSecond)

Calls first and, once it answers, calls second and connects the two. The dial plan carries on straight away and the calls are placed afterwards, so the usual dial plan asks for a callback and then rejects the call, and the caller hangs up and is rung back. Both destinations take the same forms as a sys.Dial destination.

delaySeconds waits before calling first, up to 15, which gives the phone that asked time to hang up when it is the one being called back. Each leg rings for up to 60 seconds, or less if ringSecondsFirst and ringSecondsSecond say so; 0 means 60. The header parameters add custom SIP headers to each leg's INVITE, written as "Name: value|Other: value", or nil for none. A dial plan can ask for up to three callbacks per call, and an account can have up to five in progress at once.

The person called first hears silence while the second rings. Both phones need to support the PCMU, PCMA or G729 codec. The two calls appear in Call History as legs of the call that asked for them.

# Ring the caller back through a provider and connect them to the office.
sys.Callback("#{req.Header.From.FromURI.User}@provider1", "0061290000000@provider1", 5)
sys.Respond(480, "Calling you back")

Looking things up

These answer with a value, so they're used wherever a value can be, such as in an if, an assignment, inside #{…} in a string or as another function's argument, rather than on a line of their own. Each is looked up when the dial plan reaches it, so one inside an if branch that doesn't run isn't looked up at all. A user with no domain is on your default domain, and no user at all means the account with your own user name.

FunctionAnswers
sys.IsAvailable(), sys.IsAvailable(user), sys.IsAvailable(user, domain) true if one of your SIP accounts has a device registered right now.
sys.DoesSIPAccountExist(user), sys.DoesSIPAccountExist(user, domain) true if the SIP account exists on this server, whoever it belongs to.
sys.GetBindings(), sys.GetBindings(user, domain) The registered devices of one of your accounts, each with ContactURI, MangledContactURI, UserAgent, RemoteSIPSocket, Expiry and LastUpdate; nil for an account that isn't yours.
sys.GetCanonicalDomain(host) The domain a host name stands for on this server, or nil when it isn't one of its domains.
sys.GetCurrentCallCount() How many of your call legs are in progress. An ordinary call is two legs: the one that arrived and the one placed for it.
sys.GetCurrentCalls() Those legs, each with CallId, Direction, LocalUserField, RemoteUserField, BridgeId and Inserted.
sys.GetTimezone() The time zone in your settings, such as Europe/Dublin, or nil if you haven't set one.
sys.GetTimezoneOffsetMinutes() How many minutes that time zone is ahead of UTC right now, summer time included: 60 for Dublin in July. 0 if you haven't set one.
sys.ENUMLookup("number.domain") The SIP address published in ENUM for a number, such as sys.ENUMLookup("35312345678.e164.arpa"), or nil if there isn't one. A call's dial plan can make up to 10 lookups.
# Ring the handset if it's registered, otherwise send the call to a mobile.
if sys.IsAvailable("fred")
  sys.Dial("fred@local")
else
  sys.Dial("0061400000000@provider1")
end

# Log where each of an account's devices is registered.
sys.GetBindings("fred", "sipsorcery.com").each do |binding|
  sys.Log("#{binding.UserAgent} at #{binding.ContactURI}")
end

# Send the call to voicemail when you're already on two calls. An ordinary call is two legs.
if sys.GetCurrentCallCount() >= 4
  sys.Dial("voicemail@local")
end

Each answer is shown in the call's trace in Call History. The editor's test run gives fixed answers instead: nobody available, no bindings and no calls. The time zone functions answer from your settings, as they do on a call.

The language

Besides the functions above, Ruby dial plans support what's needed for routing calls:

  • if, elsif, unless and case/when, with ==, !=, <, <=, >, >=, &&, ||, ! and ? :,
  • regular expressions, with =~, the captures $1 to $9, and Ruby's /i to ignore case and /m to let . match across lines, such as /x-note: (.*)/mi,
  • local variables, strings joined with + or <<, and #{…} in strings, which can hold any value including one with its own quotes, such as "#{names["fred"]}",
  • arrays, hashes and %w[…], .each over them and over request collections, list.map! { |x| … } to change every entry of a list, and Hash[*list] to turn a list of keys and values into a hash, as in a speed-dial table,
  • a trailing if or unless on any line, such as sys.Dial("${dst}@local") if num =~ /^\d{3}$/,
  • helpers of your own, written with def, which can be used as values or as lines of their own. A parameter can have a default, def dialler(tocall, timer=60), and a last *args parameter collects any arguments left over as an array,
  • defined?(NAME), to check whether a variable or constant has been set,
  • begin/rescue to carry on after an error, with the error's message in $! inside the rescue,
  • gsub with a block, run for each match with $1 to $9 set from it, and .hex and .chr, so num.gsub(/%(..)/) { $1.hex.chr } decodes a %40 into an @, and
  • a UTC clock with Time.now.utc, and your own time zone's offset with sys.GetTimezoneOffsetMinutes().

Every value takes the same methods, whether it's a variable, a request value such as req.URI.User, a lookup's answer or another method's result: .to_s, .length, .downcase, .strip, .include?, .start_with?, .match, .sub, .gsub, .split, .slice, .index, .scan, .map, .join, .first, .last, .empty? and .nil?, and an index can follow any of them. So req.URI.User.gsub(/-/, "").length and req.URI.User.split("_")[1] work without a variable.

There are no general loops, and no access to files, the web or the rest of the server, so a dial plan can't hold up a call for long or affect anyone else's; see Limits. Something the language doesn't support is reported with its line and column when you check the dial plan's syntax in the editor. A dial plan that doesn't compile fails every call through it, so check it after each change.

# Office hours, Monday to Friday 9am to 5pm, in the time zone in your settings.
local = Time.now.utc + (sys.GetTimezoneOffsetMinutes() * 60)
weekday = [1, 2, 3, 4, 5].include? local.wday
office_hours = [9, 10, 11, 12, 13, 14, 15, 16].include? local.hour

if weekday && office_hours
  sys.Dial("fred@local")
else
  sys.Dial("177722xxxxx@in.callcentric.com")
end
# A helper that turns 00 into + and drops anything that isn't a digit.
def e164(number)
  return number.sub(/^00/, "+").gsub(/[^+0-9]/, "")
end

sys.Log("Calling #{e164(req.URI.User)}")
sys.Dial("#{e164(req.URI.User)}@provider1")

Limits

  • Dial plans running at once. Your account can run a limited number of dial plans at the same time, across all its dial plans and SIP accounts. The limit, and how many are running now, are on your Settings page. A call that arrives when they're all in use is rejected with 480 Too many simultaneous calls, and its Call History entry says why. A dial plan counts while its call is being set up, until the call is answered or turned away, not for the rest of the conversation.
  • Each call's dial plan can run at most 10,000 lines, counting each pass of an .each and each helper's lines again, and helpers can call other helpers at most 32 deep. A dial plan that goes past either is stopped with an error, which rescue doesn't catch.
  • sys.Email: three emails per call and 30 an hour. sys.Callback: three per call and five in progress at once.

Read only dial plans and providers

SIPSorcery can mark a dial plan or a SIP provider as read only, for example while a problem with it is looked into. It's shown with a read only badge, and it can't be edited or deleted in the portal.

  • A read only dial plan doesn't run. A call through it is rejected with 403 Dialplan is read only, and it can't be chosen as a SIP account's dial plan. An account already using it keeps it, and can be changed to another dial plan.
  • A read only SIP provider doesn't register, and calls aren't placed through it. A destination in a dial string that uses it is skipped, and the rest of that part of the dial string rings as usual; if nothing else in that part can be called, it fails and the dial plan carries on with the next line, as after any failed sys.Dial. The trace says which provider was skipped and why. sys.SendRequest and sys.Callback don't send through it either, and the Simple Wizard doesn't let a rule use it.

Json dial plans

A Json dial plan is a list of rules. The rules are tried in order and the first one that matches the call decides what happens to it; a call that matches no rule is declined. A rule can match on the direction of the call, the number dialled, the caller, and the day and time, and then either dial or reject:

{
  "rules": [
    { "description": "Speed dial", "direction": "Out", "toPattern": "100", "toPatternType": "Exact",
      "action": "Dial", "targets": "subaccount1@local" },
    { "description": "International", "direction": "Out", "toPattern": "_00X", "toPatternType": "Prefix",
      "action": "Dial", "targets": "${dst}@international_pstn_provider" },
    { "description": "Office hours", "direction": "In",
      "days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
      "startTime": "09:00", "endTime": "17:30", "timeZone": "Europe/Dublin",
      "action": "Dial", "targets": "reception@local&sales@local|voicemail@local", "ringTimeoutSeconds": 25 },
    { "action": "Reject", "status": 404, "reason": "No rule for that number" }
  ]
}

The Simple Wizard writes this kind of dial plan for you, and is the easier way to build one.

Moving a dial plan from the previous SIPSorcery

Ruby dial plans from the previous version of SIPSorcery mostly run unchanged, but a few of its functions no longer exist and a few work slightly differently. Checking the syntax of a dial plan that uses a function that's gone reports the line it's on, and until it's changed every call through the dial plan fails. A function that's kept but does nothing is shown as a warning. Both are listed below, with what to use instead.

Previous function or optionNow
sys.Dial third parameter (answered call limit), cd= optionNot available: an answered call can't be given a time limit.
tr= (transfers) optionNot available, and a dial string using it fails when the call is placed. Transfers still work: a transfer request from either end is passed to the other end's phone, which decides whether to accept it. SIPSorcery never places a call itself because the far end asked it to.
A redirect (3xx response) to a destination with no rm= optionNo longer followed: it counts as a failure, and the dial string carries on. The previous SIPSorcery followed redirects unless told not to. Add [rm=a] to a destination that needs its redirect followed.
rm=n (run the dial plan again for a redirect)Not available. rm=a calls the redirect's address instead.
ma= optionAccepted and has no effect: the servers never change a call's media addresses.
ac= option (account code)Accepted and has no effect, with a warning: calls aren't billed here.
sys.LastDialled, sys.RedirectURINot available. A failed sys.Dial carries on to the next line.
sys.TraceAccepted and has no effect, with a warning: every call's trace is recorded in Call History.
requireAccepted and ignored, with a warning: Ruby libraries aren't available.
sys.Respond with a 2xx statusNot sent, with a warning: only an answered sys.Dial answers a call.
sys.WebGetNot available.
sys.DBWrite, sys.DBRead, sys.DBDelete, sys.DBGetKeys, the transaction functions and external databasesNot available. Small lookup tables can be written into the dial plan as hashes.
sys.GoogleVoiceCall, sys.GoogleContactLookup, sys.GTalkNot available. Google shut down the services they used, so they had stopped working on the previous SIPSorcery too.
sys.ExtendScriptTimeoutAccepted and has no effect, with a warning: a dial plan isn't stopped by a script timer, only by how long the call can take to set up.
sys.Email to any addressOnly your own address; see sys.Email.

Every other function and placeholder is available and works as it did, with a few small improvements that don't need a dial plan to change:

  • ${fromname}, ${fromuriuser} and ${fromaor} in a dial string have the characters | & [ ] , = removed from what the caller sent.
  • sys.SetCustomSIPHeader with a header name set earlier replaces that header rather than adding a second one with the same name.
  • sys.RemoveCustomSIPHeader takes the header's name. The whole "Name: value" line, which is what used to be needed, still works.
  • sys.GetTimezone gives the standard name of your time zone, such as Europe/Dublin, rather than a Windows display name such as (UTC) Dublin, Edinburgh, Lisbon, London.