Home / Blog / Strategy
Strategy · 6 min read · Published 2026-10-17 · OTP Route Engineering

Country Routing for OTP — Choosing Routes, Prices and Fallbacks

How to choose a country route per service for OTP verification, balance availability against price, implement fallback routing in code and stay compliant.

TL;DR — Country routing for OTP means keeping an ordered list of countries per service, filtering by live availability and a price ceiling, falling back on 409 or expiry, and reordering routes from your own receive-rate data.

Country routing for OTP is the practice of deciding, per service, which country's numbers to request first and what to do when that choice is unavailable or underperforms. A sound strategy keeps an ordered preference list per service, filters it by live availability and a price ceiling from the catalog, falls back to the next country on HTTP 409 or an expired activation, and reorders routes over time based on your own receive rates. It also respects the regulatory and platform rules that apply to each country.

This guide covers the inputs to a routing decision, the trade-off between availability and price, a working fallback implementation, and the compliance questions to settle before you go live.

What a route is

In OTP Route, a route is effectively a pair of a service and a country. For each pair the catalog tells you whether it is supported and what it costs:

  • GET /v1/services lists supported services.
  • GET /v1/countries lists supported countries. The countries page shows the same coverage for humans.
  • GET /v1/prices?service=&country= returns the current price for a specific pair.

Creating an activation with POST /v1/activations {service, country} is the moment the route is used. If no numbers are available for that pair at that moment, the API returns HTTP 409, and that response is the most important signal in any routing system.

Inputs to a routing decision

Input Source Changes how often
Supported pairs Catalog endpoints Occasionally
Price per pair GET /v1/prices Occasionally
Momentary availability 409 responses on create Constantly
Receive rate Your own activation outcomes Continuously, measured by you
Time to code Your own timestamps Continuously, measured by you
Flow expectations The service and market you are testing Rarely
Regulatory and platform constraints Legal review and platform terms Rarely, but critically

The first three come from the API. The next two come from your own data and are the inputs most teams neglect. The last two are constraints rather than preferences, so they decide which countries are eligible at all.

Availability versus price

Every routing strategy trades availability against cost. The right balance depends on what a failure costs you. A failed activation is refunded, so the money is recovered, but time is not. In a blocking CI run, a delayed verification holds up a pipeline. In an overnight provisioning batch, it may not matter.

Strategy How it orders routes Good for Watch out for
Cheapest first Ascending price Large, non-urgent batches Lower-priced routes may be busier or slower for some services
Availability first Routes with the best recent success Blocking pipelines, interactive QA Higher spend per activation
Preference with ceiling Fixed order, skipping anything above a price cap Most teams, most of the time Lists go stale without review
Weighted by outcomes Score from receive rate, time to code and price Mature, high-volume setups Needs enough data per route to be meaningful

A practical starting point is preference with a ceiling. Pick a primary country that matches the market of the flow you are testing, add one or two fallbacks, set a maximum price per activation, and review the order monthly using your own metrics.

Building a route table from the catalog

Refresh a local route table on a schedule, for example hourly, rather than calling the catalog on every activation:

import os
import requests

API = "https://otproute.com/api/v1"
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {os.environ['OTPROUTE_API_KEY']}"})

PREFERENCES = {
    # service identifier -> ordered country identifiers (placeholders; use catalog values)
    "example-service": ["us", "ca", "gb"],
}
PRICE_CEILING = {"example-service": 1.00}  # USD per activation, set by you


def build_route_table():
    table = {}
    for service, countries in PREFERENCES.items():
        routes = []
        for country in countries:
            r = session.get(f"{API}/prices",
                            params={"service": service, "country": country}, timeout=10)
            if r.status_code != 200:
                continue  # pair not supported or temporarily unavailable
            price = float(r.json()["price"])  # field name per the schema in /docs
            if price <= PRICE_CEILING[service]:
                routes.append({"country": country, "price": price})
        table[service] = routes  # preserves preference order
    return table

Fallback routing logic

The router walks the route table in order and decides at each step whether to stop, move on or wait. The rules map directly to API responses and activation outcomes:

  • HTTP 409 means no numbers are available on this route right now. Move to the next country immediately.
  • HTTP 402 means insufficient balance. Stop routing entirely, because no other country will succeed either.
  • HTTP 429 means you are being rate limited. Back off and retry the same route. This is not a routing failure.
  • Expired or soft-deadline timeout means the route did not deliver this time. Cancel the activation, which is refunded if no SMS arrived, and try the next country.
import random
import time


class StopRouting(Exception):
    pass


def create_activation(service, country, attempts=5):
    for attempt in range(attempts):
        r = session.post(f"{API}/activations",
                         json={"service": service, "country": country}, timeout=(5, 30))
        if r.status_code == 429:
            time.sleep(min(2 ** attempt, 30) + random.uniform(0, 0.5))
            continue
        if r.status_code == 402:
            raise StopRouting("insufficient balance")
        if r.status_code == 409:
            return None  # route unavailable now
        r.raise_for_status()
        return r.json()
    return None


def route_and_verify(service, route_table, submit_number, wait_for_code, soft_deadline_s):
    for route in route_table.get(service, []):
        activation = create_activation(service, route["country"])
        if activation is None:
            record_outcome(service, route["country"], "unavailable")
            continue

        submit_number(activation["number"])                 # illustrative field names
        code = wait_for_code(activation["id"], soft_deadline_s)
        if code:
            record_outcome(service, route["country"], "received")
            return code, route["country"]

        session.post(f"{API}/activations/{activation['id']}/cancel", timeout=10)
        record_outcome(service, route["country"], "no_sms")

    raise StopRouting(f"no route delivered a code for {service}")

wait_for_code should read from your webhook store, populated by verified activation.sms_received events, as described in webhook-based OTP capture. record_outcome feeds the metrics that keep your preference list honest.

Learning from outcomes

A static preference list decays. Use your recorded outcomes to adjust it:

Signal on a route Suggested action
Frequent 409 responses Demote temporarily, or move it after a route with steadier availability
Receive rate drops while other routes hold Demote and investigate; see reducing failed OTP deliveries
Time to code grows Shorten its soft deadline or demote for latency-sensitive jobs
Price rises above the ceiling The route table refresh drops it automatically
A new country appears in the catalog Trial it at low volume before promoting it

Require a minimum sample size before reordering, so that a handful of unlucky attempts does not reshuffle the table. For high-volume workloads, the bulk verification guide shows how to collect these counters per service, country and API key.

Regulatory and platform considerations

Routing is not only an engineering decision. Before you add a country to a preference list, settle these questions with whoever owns compliance in your organization:

  • Platform terms. Many services restrict how accounts may be created or verified, and some tie eligibility to the user's real location. Choose countries that are consistent with the terms of the platform and the purpose of the test.
  • Telecom rules by country. Countries differ in how phone numbers may be assigned and used, and some require identity checks for certain number types. Rules change, so treat this as an ongoing review rather than a one-off.
  • Messaging rules for dedicated lines. If you use dedicated virtual numbers in the United States or Canada for outbound SMS, follow the applicable carrier and consent requirements for the messages you send.
  • Data protection. Verification messages can contain personal data. Define how long you retain codes and message bodies, restrict who can see them, and include webhook payloads in your data inventory.
  • Auditability. Separate API keys per workload, with usage logs per key, make it possible to show which team used which routes and why.

None of this is legal advice. It is a checklist to make sure routing choices are reviewed by the right people.

Temporary routes versus dedicated lines

Country routing applies to temporary activations, where each verification can take a different path. When a workflow needs the same number over time, such as a long-lived test account or an operational inbox, routing gives way to a fixed choice. Rent a dedicated US or Canada line, receive number.sms_received events by webhook, and release it when the need ends. Compare options on the pricing page and in the OTP gateway overview.

Responsible use

Choose routes to make legitimate verification reliable, for QA and testing, privacy-preserving sign-ups, app verification flows and operational lines. Do not use country selection to misrepresent location in breach of platform rules, and never use OTP Route for fraud, spam or abuse.

Frequently asked questions

How do I choose the first country for a service?

Start with the country that matches the market the flow under test is designed for, confirm the pair is supported and priced within your ceiling using GET /v1/prices, then add one or two fallbacks. Adjust the order once you have your own receive-rate data.

What should happen when the API returns 409?

Move to the next country in your route list immediately. A 409 means no numbers are currently available for that service and country, so retrying the same route right away rarely helps. Record the event so frequent 409s can demote the route.

Is the cheapest country always the best choice?

Not necessarily. Price is only one input. For blocking pipelines, availability and time to code often matter more, while cheapest-first can suit large non-urgent batches. Failed activations are refunded, but the time they consume is not.

How often should I refresh prices and availability?

Refresh the route table from the catalog on a schedule, such as hourly, and treat 409 responses as real-time availability signals. Calling the price endpoint before every single activation adds latency and request volume without much benefit.

Are there legal issues with using numbers from other countries?

There can be. Platform terms, national telecom rules and data protection obligations all apply. Review each country with your compliance owner before adding it to a route list, and keep usage within the terms of the platforms you verify against.

Route your first OTP in minutes

Pay as you go · no monthly fee · automatic refunds on unused activations.

Topics: otp country routing · otp routing strategy · otp fallback routing · sms verification country · otp price by country · otp availability · virtual number country