Integrating an OTP API takes a handful of calls in a fixed order. Authenticate with a Bearer key, look up the price for a service and country, create an activation, wait for the code by webhook or by polling, and cancel the activation if you no longer need it. This guide walks through each step against the OTP Route API at https://otproute.com/api/v1, with working examples in curl, Python (requests) and Node.js (built-in fetch), followed by production-grade error handling for HTTP 402, 409 and 429.
If you are new to the concept, read what an OTP gateway is first. Exact request and response schemas live in the API reference. Response field names in the examples below are illustrative, so confirm them against the reference before you ship.
Prerequisites
- An OTP Route account with a funded USD wallet. You can top up by card through Stripe or with USDT or USDC.
- An API key from the developer console. Keys begin with
or_live_. Create separate keys for local development, CI and production so you can rotate or revoke one without affecting the others. - Python 3.9 or later with
requests, or Node.js 18 or later for nativefetch.
Store the key in an environment variable, never in source control:
export OTPROUTE_API_KEY="or_live_xxxxxxxxxxxxxxxx"
Step 1 — Authenticate and check your balance
Every request carries the key in the Authorization header. GET /v1/balance is a good first call because it confirms the key works and shows how much you can spend.
curl https://otproute.com/api/v1/balance \
-H "Authorization: Bearer $OTPROUTE_API_KEY"
import os
import requests
API = "https://otproute.com/api/v1"
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {os.environ['OTPROUTE_API_KEY']}"})
print(session.get(f"{API}/balance", timeout=10).json())
const API = "https://otproute.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.OTPROUTE_API_KEY}`,
"Content-Type": "application/json",
};
const res = await fetch(`${API}/balance`, { headers });
console.log(await res.json());
Step 2 — Discover services and countries
Service and country identifiers come from the catalog. Do not hard-code them from memory, because the catalog changes over time.
curl https://otproute.com/api/v1/services -H "Authorization: Bearer $OTPROUTE_API_KEY"
curl https://otproute.com/api/v1/countries -H "Authorization: Bearer $OTPROUTE_API_KEY"
Cache these responses for a short period in your application rather than calling them before every activation. The public countries page is useful for a human-readable overview.
Step 3 — Look up the price
Prices depend on both the service and the country. Check before you buy, especially in automated jobs that run unattended.
curl "https://otproute.com/api/v1/prices?service=example-service&country=us" \
-H "Authorization: Bearer $OTPROUTE_API_KEY"
price = session.get(f"{API}/prices",
params={"service": "example-service", "country": "us"},
timeout=10).json()
const params = new URLSearchParams({ service: "example-service", country: "us" });
const price = await (await fetch(`${API}/prices?${params}`, { headers })).json();
A useful pattern is to enforce a price ceiling in code. If the returned price exceeds what the job is allowed to spend, skip the combination or try a fallback country. See the pricing page for how billing works.
Step 4 — Create an activation
POST /v1/activations allocates a temporary number for the requested service and country and charges the activation price to your wallet.
curl -X POST https://otproute.com/api/v1/activations \
-H "Authorization: Bearer $OTPROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"service": "example-service", "country": "us"}'
resp = session.post(f"{API}/activations",
json={"service": "example-service", "country": "us"},
timeout=(5, 30))
resp.raise_for_status()
activation = resp.json()
activation_id = activation["id"] # see /docs for the full schema
phone_number = activation["number"]
const created = await fetch(`${API}/activations`, {
method: "POST",
headers,
body: JSON.stringify({ service: "example-service", country: "us" }),
});
if (!created.ok) throw new Error(`Create failed: ${created.status}`);
const activation = await created.json();
The activation starts in pending. Enter the allocated number into the flow you are testing promptly. The activation window is typically 20 minutes, and the clock starts at creation.
Step 5 — Receive the code
You have two options. Polling is simpler to prototype; webhooks are better in production.
| Approach | How it works | Best for |
|---|---|---|
| Polling | Call GET /v1/activations/{id} on an interval until the state changes |
Local scripts, prototypes, environments without a public endpoint |
| Webhook | OTP Route POSTs activation.sms_received to your endpoint |
Production, CI at volume, anything latency-sensitive |
Option A — Polling
Poll with a modest interval and a hard deadline. Do not poll in a tight loop, because it wastes your rate limit.
import time
def wait_for_code(activation_id, timeout_s=20 * 60, interval_s=5):
deadline = time.time() + timeout_s
while time.time() < deadline:
a = session.get(f"{API}/activations/{activation_id}", timeout=10).json()
if a["status"] == "received":
return a["code"]
if a["status"] in ("expired", "cancelled"):
return None # refunded automatically
time.sleep(interval_s)
return None
async function waitForCode(id, timeoutMs = 20 * 60 * 1000, intervalMs = 5000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const a = await (await fetch(`${API}/activations/${id}`, { headers })).json();
if (a.status === "received") return a.code;
if (a.status === "expired" || a.status === "cancelled") return null;
await new Promise((r) => setTimeout(r, intervalMs));
}
return null;
}
Option B — Webhooks
Configure a webhook endpoint in the developer console and use its test-send feature to confirm your receiver works. When an SMS arrives, OTP Route POSTs an activation.sms_received event to your URL. Related events are activation.expired and activation.cancelled, and number.sms_received for dedicated virtual numbers.
Every delivery includes this header:
X-OTPRoute-Signature: t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
Your receiver must compute the HMAC over the timestamp, a dot and the raw request body using your webhook secret, compare it in constant time, and reject stale timestamps. Full receiver code in Python and Node.js is in the webhook-based OTP capture guide.
Step 6 — Cancel what you do not need
If a test fails before it submits the number, or you decide not to proceed, cancel the activation. Cancelled activations that have not received an SMS are refunded to the wallet, and the number is released.
curl -X POST https://otproute.com/api/v1/activations/ACTIVATION_ID/cancel \
-H "Authorization: Bearer $OTPROUTE_API_KEY"
If you do nothing, an activation that never receives an SMS moves to expired at the end of its window and is refunded automatically. Cancelling simply returns the funds sooner. Once a code has been received, the charge stands and the activation proceeds to completed.
Step 7 — Handle errors deliberately
Three status codes deserve explicit handling because each one calls for a different response.
| Status | Meaning | Correct response |
|---|---|---|
| 402 Payment Required | Insufficient wallet balance | Stop creating activations; top up or alert. Retrying will not help. |
| 409 Conflict | No numbers available for that service and country right now | Try a fallback country or retry later. Do not hammer the same combination. |
| 429 Too Many Requests | Rate limit reached | Back off exponentially with jitter, honoring Retry-After if present |
| 5xx | Transient server-side error | Retry with backoff, up to a small cap |
| Other 4xx | Request problem, such as an invalid parameter or key | Log the response body and fix the request; do not retry unchanged |
A compact Python client that encodes these rules:
import random
import time
class OTPRouteError(Exception): ...
class InsufficientBalance(OTPRouteError): ...
class NoNumbersAvailable(OTPRouteError): ...
def request(method, path, max_attempts=5, **kwargs):
for attempt in range(max_attempts):
resp = session.request(method, f"{API}{path}", timeout=(5, 30), **kwargs)
if resp.status_code == 402:
raise InsufficientBalance(resp.text)
if resp.status_code == 409:
raise NoNumbersAvailable(resp.text)
if resp.status_code == 429 or resp.status_code >= 500:
wait = float(resp.headers.get("Retry-After", min(2 ** attempt, 30)))
time.sleep(wait + random.uniform(0, 0.5))
continue
resp.raise_for_status()
return resp.json()
raise OTPRouteError(f"Gave up after {max_attempts} attempts: {method} {path}")
And the Node.js equivalent:
class OTPRouteError extends Error {}
async function request(method, path, body, maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(`${API}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 402) throw new OTPRouteError("insufficient_balance");
if (res.status === 409) throw new OTPRouteError("no_numbers_available");
if (res.status === 429 || res.status >= 500) {
const retryAfter = Number(res.headers.get("Retry-After"));
const wait = retryAfter > 0 ? retryAfter * 1000 : Math.min(2 ** attempt, 30) * 1000;
await new Promise((r) => setTimeout(r, wait + Math.random() * 500));
continue;
}
if (!res.ok) throw new OTPRouteError(`HTTP ${res.status}: ${await res.text()}`);
return res.json();
}
throw new OTPRouteError(`Gave up after ${maxAttempts} attempts`);
}
One caution about retries. Be careful about automatically retrying POST /v1/activations after a timeout where you never saw a response. The first request may have succeeded. If it did and you never use the number, it will expire and refund, but your code should record the ambiguous attempt rather than silently creating a second one. The bulk verification guide covers this pattern in depth.
Putting it together
A complete flow in Python, using the helpers above:
def verify_once(service, country, submit_number):
a = request("POST", "/activations", json={"service": service, "country": country})
try:
submit_number(a["number"]) # your test step
except Exception:
request("POST", f"/activations/{a['id']}/cancel")
raise
code = wait_for_code(a["id"])
if code is None:
raise OTPRouteError("No SMS received; activation refunded")
return code
Production checklist
- Keys are stored in a secret manager, with one key per environment.
- A price ceiling is enforced before creating activations.
- 402 stops the pipeline, 409 triggers a fallback, and 429 backs off with jitter.
- Webhooks are the primary delivery path, with signatures verified on every request.
- Unused activations are cancelled promptly.
- Usage logs in the console are reviewed when receive rates change.
For QA-specific setup, see OTP for QA testing and the developer hub.
Responsible use
Use the API for testing your own applications, privacy-preserving sign-ups, verification flows you are entitled to complete, and operational lines. Respect the terms of every platform involved, and never use the service for fraud, spam or abuse.
Frequently asked questions
Which programming languages does the OTP Route API support?
Any language that can make HTTPS requests. The API is plain REST with JSON bodies and Bearer authentication, so curl, Python, Node.js, Go, Java and others all work without a dedicated SDK.
How do I know which service and country codes to send?
Call GET /v1/services and GET /v1/countries. Use the identifiers they return, and check GET /v1/prices for the specific combination before creating an activation.
What should my code do when it receives HTTP 409?
A 409 means no numbers are currently available for that service and country. Try a fallback country from your routing list or retry the same combination later with backoff. Retrying immediately in a loop rarely helps.
Is polling acceptable in production?
It works, but webhooks are recommended. Polling consumes requests for every in-flight activation and adds latency. If you poll, use an interval of several seconds and a hard deadline matching the activation window.
Am I charged if the code never arrives?
No. Activations that never receive an SMS are refunded to your wallet automatically when they expire or when you cancel them.
Route your first OTP in minutes
Pay as you go · no monthly fee · automatic refunds on unused activations.
Topics: otp api integration · otp api python · otp api node.js · receive sms api · otp api curl · verification number api · otp webhook