SBC Routing Configuration Library

Find the routing problem you need to solve on ProSBC, copy the Ruby configuration that solves it, and adapt it to your network. You can test every one free on ProSBC Lab before it goes anywhere near live calls.

How these configurations work

The library covers twelve decisions operators automate at the session border: least-cost routing, carrier failover, toll-fraud controls, rate limits, header and number normalization, per-call STIR/SHAKEN attestation and external API lookups. Each configuration states the problem, the approach and what to watch out for, since the failure modes are what you need to know before you deploy.

Routing is written as code here rather than as form fields because a configuration form can only express what whoever built it thought to anticipate, which is fine until the day your routing depends on something they did not. ProSBC exposes routing as Ruby instead, so a decision can look at anything available at call setup: the current hour, the live call count on each carrier, the answer from an external service, or whether the calling number is one you issued.

Two of these are complete routing scripts that subclass BaseRouting. The other ten are filter modules, which is how ProSBC’s own routing modules are packaged: you import the file under Routing Scripts, then add a require, an include and one filter line to your main script, usually simple_routing.rb. Each file’s header lists those lines and every NAP or route column it reads. They follow base_routing 1.42b (behavior version 2), so check your version if you run an older release.

The configurations

You can filter the list by what you’re trying to fix. Click Copy on any configuration to grab the whole file, or use its link to send a colleague straight to it.

All (12)Call routing (3)Security & fraud (3)SIP headers (2)Number formats (1)STIR/SHAKEN (2)API lookups (1)

12 configurations across 6 categories.

Least-cost routing with time-of-day rates

Call routingComplete script

Problem
You buy termination from several carriers whose rates change by hour, and you want each call to take the cheapest route available at that moment.
Approach
Store each carrier’s rates in custom route columns, then supply an ordering method that sorts the matching routes by the rate for the current hour. Matching and remapping stay standard.
Watch out for
The hour is checked on every call, so the switch from peak to off-peak rates happens on the first call after the boundary. The rates themselves are route columns, read when the configuration is activated, so changing a rate means editing the route and activating again. A route with no cost columns sorts last, which makes it a natural fallback: keep one, so a carrier with missing rates cannot strand a call.
Ruby · least_cost_routing.rbCopyLink
require 'base_routing'

# Least-cost routing with time-of-day rates. A complete routing script.
#
# Each route needs three custom route columns:
#   cost_offpeak : rate applied 00:00-06:59
#   cost_peak    : rate applied 07:00-18:59
#   cost         : rate for all other hours, and the fallback
# Hours are the SBC's local time.
class LeastCostRouting < BaseRouting

  route_match :call_field_name => :called
  route_match :call_field_name => :nap

  route_remap :call_field_name => :called, :route_field_name => :remapped_called
  route_remap :call_field_name => :nap,    :route_field_name => :remapped_nap

  route_order :method => :order_by_current_cost

  # Called for every call with the matching routes. Returns them cheapest first.
  def order_by_current_cost(routes, nap_list)
    column = rate_column_for(Time.now.hour)
    routes.sort_by { |route| cost_of(route, column) }
  end

  private

  def rate_column_for(hour)
    case hour
    when 0..6  then :cost_offpeak
    when 7..18 then :cost_peak
    else            :cost
    end
  end

  # A route with no usable cost sorts last instead of sorting as zero,
  # which would make it look like the cheapest route.
  def cost_of(route, column)
    raw = route[column] || route[:cost]
    raw.nil? ? Float::INFINITY : raw.to_f
  end
end

@@routing = LeastCostRouting.new

def init_routes(routes)
  @@routing.init routes
end

def route(call, nap_list)
  @@routing.route call, nap_list
end

Carrier failover on 503 and timeout

Call routingComplete script

Problem
When a carrier returns 503 Service Unavailable or stops responding, calls fail instead of trying the next carrier.
Approach
Return several routes in priority order and let ProSBC’s route retry work through them. Which responses move a call on to the next route is set per cause in the profile’s Reason Cause Mapping rather than in the script, so the script orders the candidates and caps how many a call may try.
Watch out for
Be deliberate about which causes continue. 503 and 408 are safe to retry, whereas 404, 486 and the 6xx class are real answers about the destination, so retrying them on another carrier wastes attempts and can reach the same destination twice. Check 603 in particular, because the default profile sets it to Continue call. A carrier that never answers is caught by the route retry timeout, which you can set globally, per NAP or per route. If you are unsure what a code means, look it up in the SIP response code decoder.
Ruby · failover_routing.rbCopyLink
require 'base_routing'

# Priority routing with a capped route-retry list. A complete routing script.
#
# Which SIP responses move a call on to the next route is not decided in the
# script. It is the "Route retry action" of each cause in the profile's
# Reason Cause Mapping (Profiles > Edit Reason Cause Mapping):
#   Continue call : 408, 500, 502, 503, 504  (this carrier could not take it)
#   Stop call     : 404, 484, 486, 6xx       (a real answer about the destination)
# A carrier that never answers is caught by the route retry timeout
# (route_retry_mode and route_retry_timeout, set globally, per NAP or per route).
#
# Route column 'priority' (integer): 0 is tried first. An empty priority also
# counts as 0, so set it on every route.
class FailoverRouting < BaseRouting

  MAX_ATTEMPTS = 3

  route_match :call_field_name => :called
  route_match :call_field_name => :nap

  route_remap :call_field_name => :called, :route_field_name => :remapped_called
  route_remap :call_field_name => :nap,    :route_field_name => :remapped_nap

  route_order :route_field_name => :priority

  after_filter :method => :cap_attempts

  # ProSBC tries the returned routes in order. Returning only the first
  # MAX_ATTEMPTS stops a bad prefix from walking the whole carrier list.
  def cap_attempts(params)
    params[:routes] = params[:routes].first(MAX_ATTEMPTS)
    params
  end
end

@@routing = FailoverRouting.new

def init_routes(routes)
  @@routing.init routes
end

def route(call, nap_list)
  @@routing.route call, nap_list
end

Skip carriers that are at their session limit

Call routingFilter module

Problem
Calls are offered to a carrier that has no capacity left, producing 503s that could have been avoided.
Approach
After matching, drop every route whose destination NAP is at or near its contracted limit, using the NAP’s live outgoing call count, so a saturated carrier is never offered a call.
Watch out for
Leave headroom rather than filling to the contracted limit exactly, because a carrier that counts sessions slightly differently will reject calls you believe are within budget. ProSBC NAPs can also enforce a hard maximum of simultaneous calls on their own, and this filter is for stepping aside before that point, with a margin you choose. For the wider picture, see these VoIP failover strategies.
Ruby · capacity_headroom.rbCopyLink
# Skip destination carriers that are at, or close to, their session limit.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'capacity_headroom'
#   include CapacityHeadroom               # inside the routing class
#   after_filter :method => :skip_full_carriers
#
# NAP column (Create New NAP Column):
#   max_outgoing_call : integer, the carrier's contracted session limit.
#                       Leave it empty on NAPs that have no limit.
module CapacityHeadroom

  HEADROOM = 0.95   # treat 95% of the limit as full

  # Called once, when the configuration is activated.
  def init_skip_full_carriers(params)
    log_trace :always, "Using CapacityHeadroom (full at #{(HEADROOM * 100).round}% of max_outgoing_call)"
  end

  # Called for every call, after matching. Keeps only routes whose
  # destination NAP still has room, using the NAP's live call count.
  def skip_full_carriers(params)
    naps = params[:naps]

    params[:routes] = params[:routes].select do |route|
      nap   = naps[route[:remapped_nap].to_s.to_sym]
      limit = nap ? nap[:max_outgoing_call].to_i : 0
      limit <= 0 || nap[:inst_outgoing_call_cnt].to_i < limit * HEADROOM
    end

    raise RoutingException, :no_circuit_available if params[:routes].empty?
    params
  end
end

Block calling numbers from a maintained list

Security & fraudFilter module

Problem
You need to stop calls from specific calling numbers or prefixes: a mandatory blocking order, a fraud pattern, or a known abusive source.
Approach
Load the list once, when the configuration is activated, then reject matching calls before any routing work happens. Entries match as prefixes as well as exact numbers, so one entry can block a whole range.
Watch out for
Load at activation, not per call. A file read on every INVITE will not survive real call rates, and updating the list then means importing the new file and activating the configuration. Log every block with the entry it matched, so you can show why a call was rejected. The rejection reason becomes a SIP response through the profile’s Reason Cause Mapping, so check what call_rejected maps to there, and keep policy blocks apart from authorization failures so your CDRs stay readable. For blocking orders that name providers, see the robocall enforcement tracker.
Ruby · calling_blocklist.rbCopyLink
# Reject calls whose calling number matches a blocklist entry.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'calling_blocklist'
#   include CallingBlocklist               # inside the routing class
#   before_filter :method => :reject_blocked_calling, :blocklist_file => 'blocklist.csv'
#
# blocklist.csv is imported as a custom file in the File DB. First line is the
# header; each entry is a full number or a prefix, with or without '+':
#   prefix,reason
#   15551230000,blocking order 2026-014
#   1555999,fraud pattern
module CallingBlocklist

  # Called once, when the configuration is activated, so no file is read on
  # the call path. DbFile.get_file raises if the file has not been imported.
  def init_reject_blocked_calling(params)
    @blocked = {}
    file = DbFile.get_file(params[:blocklist_file] || 'blocklist.csv')

    file.csv_parse do |col_names, col_vals, row_idx|
      prefix = col_vals['prefix'].to_s.strip.sub(/\A\+/, '')
      @blocked[prefix] = col_vals['reason'].to_s unless prefix.empty?
    end

    log_trace :always, "CallingBlocklist loaded #{@blocked.size} entries"
  end

  # Called for every call, before any routing work. Checks each leading part
  # of the number against the table, so the cost does not grow with the list.
  def reject_blocked_calling(params)
    calling = params[:call][:calling].to_s.sub(/\A\+/, '')
    hit = (1..calling.length).map { |n| calling[0, n] }.find { |prefix| @blocked.key?(prefix) }

    if hit
      log_trace 1, "Blocked call from #{calling}: matched #{hit} (#{@blocked[hit]})"
      raise RoutingException, :call_rejected
    end

    params
  end
end

Restrict expensive destinations by time and trunk

Security & fraudFilter module

Problem
Toll fraud typically shows up as international calls to high-cost destinations, outside business hours, from a compromised extension or PBX.
Approach
Give each trunk a NAP column listing the country codes it may reach, and narrow high-risk destinations to weekday business hours. The check runs before routing, so a compromised endpoint cannot generate cost.
Watch out for
This is the single highest-value fraud control you can add, because it caps exposure regardless of how the credential was compromised. Default-deny international and allow-list the destinations each customer genuinely calls. Country code 1 also reaches the Caribbean NANP countries, which bill at international rates, so the example list names area codes such as 876 and 809 explicitly. Most toll fraud lands overnight and at weekends, which is exactly when nobody is watching. More on real-time toll fraud prevention.
Ruby · destination_policy.rbCopyLink
# Per-trunk destination allow-list, tightened outside business hours.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'destination_policy'
#   include DestinationPolicy              # inside the routing class
#   before_filter :method => :enforce_destination_policy
#
# NAP column (Create New NAP Column), read on the incoming NAP:
#   allowed_cc : country codes the trunk may call, space separated ('1 44 33'),
#                or 'any'. Empty means '1' (domestic only, for a NANP operator).
#
# Called numbers are expected in E.164, with or without '+', at this point.
module DestinationPolicy

  # Example list, replace it with your own. Includes Caribbean NANP area codes,
  # which country code 1 also reaches. Allowed only Monday to Friday,
  # 08:00-18:59, SBC local time.
  HIGH_RISK      = %w[1876 1809 212 224 225 234 235 252 372 373 508 509 675 676 677 678 679].freeze
  BUSINESS_DAYS  = (1..5)    # Time#wday: 0 is Sunday
  BUSINESS_HOURS = (8..18)

  def init_enforce_destination_policy(params)
    log_trace :always, "Using DestinationPolicy (#{HIGH_RISK.size} high-risk prefixes)"
  end

  def enforce_destination_policy(params)
    call   = params[:call]
    nap    = params[:naps][call[:nap].to_s.to_sym] || {}
    called = call[:called].to_s.sub(/\A\+/, '')

    allowed = nap[:allowed_cc].to_s.split
    allowed = ['1'] if allowed.empty?

    unless allowed.include?('any') || allowed.any? { |cc| called.start_with?(cc) }
      log_trace 1, "Denied #{called} on #{call[:nap]}: destination not permitted"
      raise RoutingException, :call_rejected
    end

    now = Time.now
    in_hours = BUSINESS_DAYS.cover?(now.wday) && BUSINESS_HOURS.cover?(now.hour)
    if HIGH_RISK.any? { |cc| called.start_with?(cc) } && !in_hours
      log_trace 1, "Denied #{called} on #{call[:nap]}: high-risk destination out of hours"
      raise RoutingException, :call_rejected
    end

    params
  end
end

Throttle call attempts per calling number

Security & fraudFilter module

Problem
One calling number starts placing calls faster than any person can dial, from a looping dialer or a hijacked extension, while the rest of its trunk is normal. A per-trunk cap either misses it or slows every other customer on that trunk.
Approach
Keep a one-second sliding window of attempts per calling number and reject attempts past the limit, so one source is slowed without touching its neighbors.
Watch out for
For a plain per-trunk limit, use the NAP’s built-in call rate limiting (a maximum calls per second plus a burst allowance) rather than a script, and keep this for limits the NAP setting cannot express. The window lives in the script’s memory, so it starts empty whenever the configuration is activated, and idle numbers are pruned so the table cannot grow without bound. Floods that do not come from a single number need SIP DoS protection at the SBC.
Ruby · calling_rate_limit.rbCopyLink
# Sliding-window limit on call attempts per calling number.
#
# For a plain per-trunk limit, use the NAP's own call rate limiting
# (maximum calls per second and maximum burst) instead of a script.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'calling_rate_limit'
#   include CallingRateLimit               # inside the routing class
#   before_filter :method => :limit_calling_rate
module CallingRateLimit

  MAX_ATTEMPTS = 5      # attempts allowed per calling number...
  WINDOW       = 1.0    # ...in this many seconds
  PRUNE_EVERY  = 1000   # calls between clean-ups of idle numbers

  # Called when the configuration is activated, so the window starts empty.
  def init_limit_calling_rate(params)
    @attempts = {}
    @calls    = 0
    log_trace :always, "Using CallingRateLimit (#{MAX_ATTEMPTS} per #{WINDOW}s per calling number)"
  end

  def limit_calling_rate(params)
    calling = params[:call][:calling].to_s
    return params if calling.empty?

    now    = Time.now.to_f
    recent = (@attempts[calling] ||= [])
    recent.reject! { |t| now - t > WINDOW }

    if recent.size >= MAX_ATTEMPTS
      log_trace 1, "Rate limit hit for #{calling}: #{recent.size} attempts in #{WINDOW}s"
      raise RoutingException, :no_circuit_available
    end

    recent << now
    @calls += 1
    prune_idle(now) if @calls % PRUNE_EVERY == 0
    params
  end

  private

  # Forget numbers with no attempt inside the window, so the table stays small.
  def prune_idle(now)
    @attempts.delete_if { |number, times| times.empty? || now - times.last > WINDOW }
  end
end

Normalize P-Asserted-Identity per carrier

SIP headersFilter module

Problem
One carrier wants the calling identity in P-Asserted-Identity, another only reads From, and a third rejects the call if PAI is present at all.
Approach
Read a policy from a column on each destination NAP: build a PAI from the calling number where the carrier requires one, remove it where the carrier objects, and pass it through everywhere else.
Watch out for
Get this right before you troubleshoot attestation problems. The signing service takes the calling identity from PAI when one is present, so a malformed PAI shows up as what looks like an attestation fault. ProSBC presents PAI to the script as the private_address fields rather than as a raw header, which is why this edits those fields. More on SIP header manipulation.
Ruby · pai_policy.rbCopyLink
# Per-carrier P-Asserted-Identity policy.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'pai_policy'
#   include PaiPolicy                      # inside the routing class
#   after_remap_filter :method => :apply_pai_policy
#
# NAP columns (Create New NAP Column), read on the destination NAP:
#   pai_policy : build|strip|pass. Empty means pass.
#                build : assert the calling number in P-Asserted-Identity
#                strip : send no P-Asserted-Identity or P-Preferred-Identity
#                pass  : forward whatever arrived
#   pai_domain : optional host for the built identity, e.g. carrier.example.net
module PaiPolicy

  def init_apply_pai_policy(params)
    log_trace :always, "Using PaiPolicy"
  end

  # Called once per call, after remapping. out_calls[i] is the outgoing call
  # that will be made on routes[i], so each carrier gets its own policy.
  def apply_pai_policy(params)
    params[:routes].each_with_index do |route, i|
      out_call = params[:out_calls][i]
      nap      = params[:naps][route[:remapped_nap].to_s.to_sym] || {}

      case nap[:pai_policy].to_s.downcase
      when 'build'
        calling = out_call[:calling].to_s
        next if calling.empty?
        out_call[:private_address]          = calling
        out_call[:private_address_sip_host] = nap[:pai_domain] unless nap[:pai_domain].to_s.empty?
        log_trace 2, "PAI built for #{route[:remapped_nap]}: #{calling}"
      when 'strip'
        out_call[:private_address] = ''
        out_call[:preferred_id]    = ''
        log_trace 2, "PAI stripped for #{route[:remapped_nap]}"
      end
    end

    params
  end
end

Add user=phone for carriers that require it

SIP headersFilter module

Problem
A carrier rejects or misroutes calls unless the SIP URIs carry user=phone, while your other carriers neither need nor want it.
Approach
After remapping, add user=phone to the From, To and P-Asserted-Identity URI parameters on calls to the carriers flagged in a NAP column, keeping every parameter already there.
Watch out for
ProSBC hands the script each header’s parameters as three strings (user, URI and header parameters), so this appends to the URI parameters instead of rebuilding the header. Headers the SIP stack processes itself, such as Require, Supported and Privacy, never appear in the script’s custom header field, so a routing script cannot rewrite them as raw headers. The script parameter reference lists the fields a script can change.
Ruby · user_phone.rbCopyLink
# Add user=phone to the From, To and P-Asserted-Identity URIs, per carrier.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'user_phone'
#   include UserPhone                      # inside the routing class
#   after_remap_filter :method => :add_user_phone
#
# NAP column (Create New NAP Column), read on the destination NAP:
#   add_user_phone : boolean. Set it on the carriers that require user=phone.
module UserPhone

  # SIP header parameter fields: From, To and P-Asserted-Identity.
  FIELDS = [:calling_parameters, :called_parameters, :private_address_parameters].freeze

  def init_add_user_phone(params)
    log_trace :always, "Using UserPhone"
  end

  def add_user_phone(params)
    params[:routes].each_with_index do |route, i|
      nap = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
      next unless route_col_true?(nap[:add_user_phone])

      out_call = params[:out_calls][i]
      FIELDS.each do |field|
        next if field == :private_address_parameters && out_call[:private_address].to_s.empty?

        # Each field is a hash of :user_param, :uri_param and :header_param strings.
        sip_params = out_call[field].is_a?(Hash) ? out_call[field] : {}
        uri = sip_params[:uri_param].to_s
        next if uri.split(';').include?('user=phone')

        sip_params[:uri_param] = uri.empty? ? 'user=phone' : "#{uri};user=phone"
        out_call[field] = sip_params
      end
      log_trace 2, "user=phone added for #{route[:remapped_nap]}"
    end

    params
  end
end

Normalize every number to E.164 on egress

Number formatsFilter module

Problem
404 Not Found or 484 Address Incomplete, because one carrier wants +1XXXXXXXXXX, another 1XXXXXXXXXX and a third the bare 10 digits.
Approach
Canonicalize each called and calling number to E.164 internally, then format it on egress the way each destination NAP’s column says.
Watch out for
Normalize in, format out. One canonical internal representation is what keeps this maintainable as trunks are added, while translating directly between carrier formats produces a rule matrix that grows quadratically. Anything that is not a number, such as anonymous, is left alone. More on the E.164 number format.
Ruby · e164_format.rbCopyLink
# Canonicalize numbers to E.164, then format them the way each carrier expects.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'e164_format'
#   include E164Format                     # inside the routing class
#   after_remap_filter :method => :format_numbers
#
# NAP column (Create New NAP Column), read on the destination NAP:
#   number_format : plus|cc|nsn. Empty means plus.
#     plus -> +15551234567
#     cc   -> 15551234567
#     nsn  -> 5551234567   (national significant number)
module E164Format

  DEFAULT_CC = '1'

  def init_format_numbers(params)
    log_trace :always, "Using E164Format (default country code #{DEFAULT_CC})"
  end

  # Runs after the route's own remapping, once per outgoing call.
  def format_numbers(params)
    params[:routes].each_with_index do |route, i|
      out_call = params[:out_calls][i]
      nap      = params[:naps][route[:remapped_nap].to_s.to_sym] || {}
      style    = nap[:number_format].to_s.downcase
      style    = 'plus' if style.empty?

      [:called, :calling].each do |field|
        e164 = e164_canonical(out_call[field])
        out_call[field] = e164_formatted(e164, style) unless e164.empty?
      end
    end

    params
  end

  private

  # Digits only, with the country code present. Anything that is not a
  # number (anonymous, alphanumeric) comes back empty and is left alone.
  def e164_canonical(number)
    digits = number.to_s.gsub(/[^0-9+]/, '')
    return '' if digits.delete('+').empty?

    if digits.start_with?('+')
      digits[1..-1]
    elsif digits.start_with?('011')       # NANP international prefix
      digits[3..-1]
    elsif digits.length == 10             # bare NANP national number
      DEFAULT_CC + digits
    else
      digits
    end
  end

  def e164_formatted(e164, style)
    case style
    when 'plus' then "+#{e164}"
    when 'nsn'  then e164.start_with?(DEFAULT_CC) ? e164[DEFAULT_CC.length..-1] : e164
    else             e164
    end
  end
end

Choose attestation A, B or C per call

STIR/SHAKENFilter module

Problem
One SBC carries retail, wholesale and gateway traffic. A single per-trunk attestation setting is either a false claim on some calls or an unnecessary downgrade on others.
Approach
Decide the level from the ingress trunk’s traffic class and whether the calling number is one you issued to that customer, then send the decision only on the INVITE to the signing service, stripping any level a customer tried to send.
Watch out for
Attesting at A level for a number you cannot verify is a false claim under FCC rules, and attesting C for your own retail subscribers hurts their call completion. The decision has to be yours rather than your signing vendor’s, and FCC 24-120 makes that explicit. How the level reaches the signing service is provider-specific, so the header name in the file is a placeholder to replace with the one your STI-AS documents. In production, ProSBC reaches its signing partners, TransNexus ClearIP and Neustar, over SIP, which is why this keys off the NAP whose service_type is AUTHENTICATION.
Ruby · attestation_policy.rbCopyLink
# Decide attestation A, B or C per call, and pass the decision to the signing
# service on the INVITE that goes to it.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'attestation_policy'
#   include AttestationPolicy              # inside the routing class
#   after_remap_filter :method => :select_attestation
#
# NAP columns (Create New NAP Column):
#   traffic_class : retail|wholesale|gateway on each incoming NAP. Empty means gateway.
#   service_type  : NORMAL|AUTHENTICATION|VERIFICATION, as in the ClearIP setup.
#                   The level is sent only to NAPs marked AUTHENTICATION.
#
# issued_numbers.csv is imported as a custom file in the File DB:
#   prefix,nap
#   1555200,retail-a
#   1555300,retail-b
module AttestationPolicy

  # PLACEHOLDER. Use the header name and values your signing service documents
  # for the requested attestation level.
  ATTEST_HEADER = 'X-Attestation-Level'

  def init_select_attestation(params)
    @issued = {}
    file = DbFile.get_file(params[:issued_file] || 'issued_numbers.csv')

    file.csv_parse do |col_names, col_vals, row_idx|
      prefix = col_vals['prefix'].to_s.strip.sub(/\A\+/, '')
      (@issued[col_vals['nap'].to_s] ||= []) << prefix unless prefix.empty?
    end

    log_trace :always, "AttestationPolicy loaded number ranges for #{@issued.size} NAPs"
  end

  def select_attestation(params)
    call    = params[:call]
    ingress = params[:naps][call[:nap].to_s.to_sym] || {}
    calling = call[:calling].to_s.sub(/\A\+/, '')

    level =
      case ingress[:traffic_class].to_s.downcase
      when 'retail'    then issued_to?(call[:nap].to_s, calling) ? 'A' : 'B'
      when 'wholesale' then 'B'
      else                  'C'
      end

    params[:routes].each_with_index do |route, i|
      out_call = params[:out_calls][i]
      dest     = params[:naps][route[:remapped_nap].to_s.to_sym] || {}

      # Never forward a level that arrived from the customer.
      headers = out_call[:sip_header].to_s.split("\n").reject { |h| attest_header?(h) }
      headers << "#{ATTEST_HEADER}: #{level}" if dest[:service_type].to_s.upcase == 'AUTHENTICATION'
      out_call[:sip_header] = headers.empty? ? '' : headers.join("\n") + "\n"
    end

    log_trace 2, "Attestation #{level} for #{calling} from #{call[:nap]}"
    params
  end

  private

  def issued_to?(nap_name, calling)
    (@issued[nap_name] || []).any? { |prefix| calling.start_with?(prefix) }
  end

  def attest_header?(line)
    line.split(':', 2).first.to_s.strip.casecmp(ATTEST_HEADER) == 0
  end
end

Route on the verification result (verstat)

STIR/SHAKENFilter module

Problem
Inbound calls arrive with a verification result and you want to treat verified, failed and unsigned traffic differently instead of ignoring it.
Approach
Read verstat from the P-Asserted-Identity or From URI parameters, then send failed calls to routes flagged for them, such as an announcement or a review queue, and let everything else route normally.
Watch out for
The distinction that matters is between TN-Validation-Failed and No-TN-Validation. Failed means a token was checked and did not pass, while No-TN-Validation means there was nothing to check, usually because the call arrived unsigned, and treating the two alike would flag enormous volumes of legitimate traffic. With TransNexus ClearIP as the verification service, verstat comes back in the P-Asserted-Identity of its 302 response. The PASSporT decoder sets out what each verstat value means.
Ruby · verstat_policy.rbCopyLink
# Route inbound calls on the STIR/SHAKEN verification result (verstat).
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'verstat_policy'
#   include VerstatPolicy                  # inside the routing class
#   after_filter :method => :branch_on_verstat
#
# Route column (Create New Route Column):
#   failed_verification : boolean. Set it on the route(s) that should take calls
#                         whose verification failed, such as an announcement
#                         or a review queue. Other calls never use those routes.
module VerstatPolicy

  def init_branch_on_verstat(params)
    log_trace :always, "Using VerstatPolicy"
  end

  def branch_on_verstat(params)
    call    = params[:call]
    verstat = read_verstat(call)

    divert = params[:routes].select { |route| route_col_true?(route[:failed_verification]) }
    normal = params[:routes] - divert

    if verstat == 'TN-Validation-Failed' && !divert.empty?
      # A token was present and did not validate. Divert rather than drop.
      log_trace 1, "Verification failed for #{call[:calling]}: diverting"
      params[:routes] = divert
    else
      # Passed, No-TN-Validation or no verstat at all. Unsigned is not
      # suspicious on its own, so normal policy decides.
      log_trace 2, "verstat #{verstat || 'absent'} for #{call[:calling]}"
      params[:routes] = normal
    end

    params
  end

  private

  # verstat is a parameter of the P-Asserted-Identity or From URI. Each
  # parameter field is a hash of :user_param, :uri_param and :header_param.
  def read_verstat(call)
    [:private_address_parameters, :calling_parameters].each do |field|
      value = call[field]
      text  = value.is_a?(Hash) ? value.values.join(';') : value.to_s

      text.split(';').each do |pair|
        name, setting = pair.split('=', 2)
        return setting.to_s.strip if name.to_s.strip.casecmp('verstat') == 0
      end
    end
    nil
  end
end

Query an external API and route on the answer

API lookupsFilter module

Problem
Routing depends on data the SBC does not hold: an LNP dip, a fraud score, a customer’s current plan, or a CRM lookup.
Approach
Use ProSBC’s HTTP query. The script describes the request and raises http_query_required, ProSBC runs it with a hard timeout, and the script runs again with the response, which it uses to remap the called number, reject the call, or put the carrier the service named first.
Watch out for
The failure policy is the whole design. The script never waits on the network itself, but the call does, so the timeout has to fit inside your post-dial delay budget. Decide in advance what a slow or failed lookup means: fail open for routing enrichment, fail closed for fraud blocking, and make the choice explicit in the filter options.
Ruby · external_route_lookup.rbCopyLink
# Ask an external service how to route a call, using ProSBC's HTTP query.
# The script does not wait on the network itself: it describes the query,
# ProSBC runs it, and the routing script runs again with the answer.
#
# Import this file under Routing Scripts (Load at Startup unchecked), then add
# to your main routing script (usually simple_routing.rb):
#   require 'external_route_lookup'
#   include ExternalRouteLookup            # inside the routing class
#   before_filter :method => :external_route_lookup,
#                 :lookup_url => 'https://routing.example.com/v1/lookup',
#                 :timeout_ms => 1500,
#                 :fail_open  => true
#   after_filter  :method => :prefer_lookup_nap
#
# Expected JSON answer: {"called": "...", "nap": "...", "block": false, "reason": "..."}
require 'uri'
require 'json'

module ExternalRouteLookup

  def init_external_route_lookup(params)
    @lookup_url = params[:lookup_url].to_s
    @timeout_ms = (params[:timeout_ms] || 1500).to_i   # keep well inside your post-dial delay budget
    @fail_open  = params.key?(:fail_open) ? params[:fail_open] : true
    log_trace :always, "Using ExternalRouteLookup: #{@lookup_url} (#{@timeout_ms} ms, fail #{@fail_open ? 'open' : 'closed'})"
  end

  def external_route_lookup(params)
    call = params[:call]

    if params[:http_query].nil?
      # First pass: describe the query and ask ProSBC to run it.
      query = URI.encode_www_form(:called => call[:called].to_s, :calling => call[:calling].to_s)
      params[:http_query] = {
        :url          => "#{@lookup_url}?#{query}",
        :use_post     => false,
        :timeout_ms   => @timeout_ms,
        :headers_hash => { 'Accept' => 'application/json' }
      }
      raise RoutingException, :http_query_required
    end

    # Second pass: the result is in params[:http_query].
    answer = parse_answer(params[:http_query])

    if answer.nil?
      log_trace 1, "Routing lookup failed: #{@fail_open ? 'routing normally' : 'rejecting'}"
      raise RoutingException, :temporary_failure unless @fail_open
      return params
    end

    if answer['block']
      log_trace 1, "Routing lookup blocked #{call[:called]}: #{answer['reason']}"
      raise RoutingException, :call_rejected
    end

    call[:called] = answer['called'].to_s if answer['called']
    if answer['nap']
      params[:user_context] ||= {}
      params[:user_context][:lookup_nap] = answer['nap'].to_s
    end
    params
  end

  # After matching: try the NAP the service named first, keep the rest as backup.
  def prefer_lookup_nap(params)
    wanted = (params[:user_context] || {})[:lookup_nap]
    return params if wanted.nil?

    first = params[:routes].select { |route| route[:remapped_nap].to_s.casecmp(wanted) == 0 }
    params[:routes] = first + (params[:routes] - first)
    params
  end

  private

  # nil unless the service answered 200 with a JSON object.
  def parse_answer(http_query)
    return nil unless http_query[:response_value].to_i == 200
    answer = JSON.parse(http_query[:response_data].to_s)
    answer.is_a?(Hash) ? answer : nil
  rescue JSON::ParserError
    nil
  end
end

We recommend validating every configuration before you deploy it, since these are starting points to adapt to your network rather than production-ready code. That step matters more for routing than for most code, because a routing script runs in the call path: if it hits an error it doesn’t handle, ProSBC refuses the call as a temporary failure. We parsed every file with Prism, Ruby’s own parser, and ran each one against a test harness built on the documented BaseRouting interface. That catches typos and logic slips, but it can’t tell you how a configuration behaves with your traffic, which is what a run on ProSBC Lab or a trial instance is for.

Have a routing problem to solve?

Tell us what your routing has to do, and we can discuss how ProSBC can fit your needs. In the meantime, ProSBC Lab gives you a free, permanent three-session instance to build and test on. For the bigger picture, read our guide to API-driven call routing.

By submitting this form, your information will be processed in accordance with our Privacy Policy.