Running OTP verification at scale means treating every activation as a short-lived, billable job rather than a one-off API call. In practice that comes down to five habits: cap concurrency, keep a job record for every activation, budget the wallet before each batch, receive codes by webhook instead of polling, and let automatic refunds absorb activations that never receive an SMS. Getting these right lets a test fleet grow without double-spending, stalling on rate limits or losing track of codes.
This guide is written for engineers who already have a working single-activation integration (if not, start with the OTP API integration guide) and now need it to hold up under real volume.
What "at scale" looks like for verification workloads
High-volume verification usually comes from one of three places, and each has a different shape.
| Workload | Typical pattern | Main risk at volume |
|---|---|---|
| QA test fleets | Bursts of parallel sign-ups during a test run | Rate limiting and contention for numbers in one country |
| Test account provisioning | Scheduled batches that create seeded accounts for staging | Budget overruns and orphaned activations |
| Operational verification | Steady background flow from internal tools | Silent failures that nobody notices for hours |
The common thread is that verification becomes a pipeline stage. A pipeline stage needs the same properties as any other distributed job: bounded resource use, traceability and safe retries. If you are building this into a release pipeline, the CI/CD solution page covers environment setup; the patterns below apply regardless of where the jobs originate.
Model each activation as a job
The single most useful design decision is to keep your own record of every activation you start. OTP Route tracks the activation state on its side, but your system needs to know which test, account or ticket that activation belongs to.
A minimal job record contains:
- A job key you generate, such as
signup-test-4812-attempt-1, unique per logical attempt. - The service and country requested.
- The activation ID returned by
POST /v1/activations, once known. - A local state that mirrors the activation lifecycle (
pending,received,completed,expired,cancelled) plus your own pre-states such asqueuedandrequesting. - Timestamps for requested, received and finished, so you can measure time to code.
Write the job record as requesting before you call the API, then update it with the activation ID when the response returns. If a worker crashes between the two, you have a visible record of an attempt with an unknown outcome instead of an invisible one.
Why orphaned activations are not a financial problem
Suppose a request times out and you never learn the activation ID. Because the number was never entered into the target service, no SMS arrives, the activation reaches expired at the end of its window (typically 20 minutes), and its price is refunded to the wallet automatically. The cost of an orphan is bounded to a temporary hold on funds and an allocated number, not a loss. You can confirm this after the fact in the usage logs in the developer console. That property is what makes aggressive timeouts safe.
Concurrency control
Unbounded parallelism is the most common cause of trouble at volume. Too many simultaneous POST /v1/activations calls will trigger HTTP 429 responses, and too many simultaneous activations in one country can exhaust available inventory, producing HTTP 409.
Use a fixed-size worker pool or semaphore, and treat the limit as a tunable parameter rather than a constant. The example below uses Python's thread pool with a shared session and a backoff helper.
import os
import random
import time
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
API = "https://otproute.com/api/v1"
MAX_CONCURRENCY = 8 # tune for your workload; start small
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {os.environ['OTPROUTE_API_KEY']}"})
class InsufficientBalance(Exception):
pass
def call(method, path, **kwargs):
"""Make a request, backing off on HTTP 429."""
for attempt in range(6):
resp = session.request(method, f"{API}{path}", timeout=(5, 30), **kwargs)
if resp.status_code != 429:
return resp
retry_after = resp.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(delay + random.uniform(0, 0.5)) # jitter avoids synchronized retries
return resp
def start_activation(job):
resp = call("POST", "/activations",
json={"service": job["service"], "country": job["country"]})
if resp.status_code == 402:
raise InsufficientBalance(job["key"])
if resp.status_code == 409:
return job["key"], "no_numbers_available", None
resp.raise_for_status()
return job["key"], "pending", resp.json()
def run_batch(jobs):
results = []
with ThreadPoolExecutor(max_workers=MAX_CONCURRENCY) as pool:
futures = [pool.submit(start_activation, j) for j in jobs]
for f in as_completed(futures):
try:
results.append(f.result())
except InsufficientBalance:
# Stop scheduling new work; in-flight activations continue.
for other in futures:
other.cancel()
break
return results
A few rules make this robust:
- Back off with jitter on 429. Honor a
Retry-Afterheader if one is present; otherwise use exponential backoff. Jitter prevents all workers from retrying at the same instant. - Treat 409 as a routing signal, not an error. No numbers are available for that combination right now. Move the job to a fallback country or requeue it later. Country routing for OTP covers fallback design.
- Treat 402 as a circuit breaker. Insufficient balance will not fix itself by retrying. Stop scheduling, alert a human or an automated top-up process, and resume afterward.
Idempotency on your side
At volume, duplicate work shows up in three places: duplicate activation requests, duplicate webhook deliveries and duplicate consumption of a code. Each needs a guard.
| Duplicate source | Guard |
|---|---|
A worker retries POST /v1/activations after an ambiguous failure |
Check the job record first; only create a new activation under a new attempt key |
| The same webhook event is delivered more than once | Deduplicate on activation ID plus event type before acting |
| Two consumers read the same received code | Transition the job from received to completed atomically, and only the winner uses the code |
Do not blindly retry the activation request after a network timeout. Record the old attempt as unknown, start a new attempt with a new key if you still need a number, and let the unknown attempt expire and refund on its own. That trades a few minutes of held funds for a guarantee that you never lose track of an activation. The webhook capture guide shows how to implement event deduplication alongside signature verification.
Wallet budgeting
OTP Route uses a prepaid USD wallet, funded by card via Stripe or by USDT and USDC stablecoins. Prices vary by service and country, and they can change, so compute budgets from live data rather than from a spreadsheet.
A simple preflight check before each batch:
- Call
GET /v1/prices?service=&country=for every combination in the batch. - Multiply each price by the number of jobs planned for that combination.
- Add headroom for retries. Refunded activations return funds, but retries are charged while they are pending.
- Compare the total with
GET /v1/balanceand either proceed, shrink the batch, or top up first.
def preflight(plan, headroom=1.25):
"""plan: list of (service, country, count). Returns (required, available)."""
required = 0.0
for service, country, count in plan:
price = call("GET", "/prices", params={"service": service, "country": country}).json()
required += float(price["price"]) * count # field names per the schema in /docs
balance = call("GET", "/balance").json()
return required * headroom, float(balance["balance"])
Field names in the snippet are illustrative; use the response schema in the API reference. The important part is the shape of the check: price from the catalog, multiplied by planned volume, compared against the live balance.
Separate keys, separate visibility
The developer console supports multiple API keys. Issue one per pipeline or environment, for example one for nightly regression and one for staging provisioning. Usage logs are then attributable per key, which makes it straightforward to see which workload is spending what, and you can revoke a single key without disrupting the rest.
Refunds and failure accounting
Refunds are automatic for activations that never receive an SMS, whether they expire or you cancel them. That shapes two operational habits.
Cancel early when you know the SMS will not come. If a test fails before it submits the phone number to the target service, call POST /v1/activations/{id}/cancel immediately. The number is released and the refund lands without waiting out the activation window.
Measure cost per successful verification, not cost per activation. Track these counters per service and country:
| Metric | How to compute it |
|---|---|
| Activations started | Count of successful POST /v1/activations calls |
| Codes received | Count of activation.sms_received events after deduplication |
| Expired and cancelled | Count of activation.expired and activation.cancelled events |
| Receive rate | Codes received divided by activations started |
| Time to code | Received timestamp minus requested timestamp |
| Net cost per success | Net wallet spend divided by codes received |
These are your own numbers, measured on your own traffic. They tell you which routes deserve priority and when a route has degraded. If receive rates drop for a combination, the guide to reducing failed OTP deliveries lists the usual causes.
A reference pipeline
Putting it together, a bulk verification system usually looks like this:
- Scheduler creates job records with job keys and enqueues them.
- Preflight checks the budget for the batch against prices and balance.
- Workers, capped by a concurrency limit, call
POST /v1/activationsand store activation IDs. - Test runners submit the allocated numbers to the target flow.
- Webhook receiver verifies
X-OTPRoute-Signature, deduplicates, and writes codes to the job store. - Consumers claim received codes atomically and complete the verification step.
- Reconciler periodically calls
GET /v1/activations/{id}for jobs stuck inpendingpast their window, as a safety net if a webhook was missed. - Reporting aggregates metrics per service, country and API key.
Each stage can fail independently without corrupting the others, which is the point.
Responsible use
Bulk verification is a legitimate engineering need for QA, test environments and operational tooling. It must not be used to mass-create accounts on third-party platforms in breach of their terms, to manipulate promotions, or for spam, fraud or any other abuse. Verify against your own applications or within the published rules of the platforms you test, and keep a clear record of what each workload is for.
Frequently asked questions
How many activations can I run in parallel?
There is no single right number. Start with a small concurrency limit, watch for HTTP 429 and HTTP 409 responses, and increase gradually. Treat the limit as configuration per workload, and consult /docs for current rate-limit guidance.
What happens to my money if an activation never receives a code?
It is refunded to your wallet automatically. When an activation expires at the end of its window, typically 20 minutes, or when you cancel it before an SMS arrives, the activation price is credited back.
Should bulk workloads use polling or webhooks?
Webhooks. Polling multiplies request volume by the number of in-flight activations and pushes you toward rate limits. Use webhooks as the primary path and a low-frequency reconciler that polls only stuck activations as a safety net.
How do I stop a batch from overspending?
Run a preflight that multiplies live prices from GET /v1/prices by planned volume, adds headroom for retries and compares the total with GET /v1/balance. Treat HTTP 402 during the run as a signal to stop scheduling new jobs.
Can I separate costs by team or pipeline?
Yes. Create a separate API key in the developer console for each pipeline or environment. Usage logs are recorded per key, so spend and errors can be attributed without extra tagging.
Route your first OTP in minutes
Pay as you go · no monthly fee · automatic refunds on unused activations.
Topics: bulk otp verification · otp at scale · high volume sms verification · otp api concurrency · test account provisioning · otp wallet budgeting · otp refunds