Home / API Reference
REST API v1

OTP Route API reference

A small, predictable JSON API for routing OTP activations, capturing codes with signed webhooks and managing dedicated numbers. Base URL: https://otproute.com/api/v1

Authentication

Create API keys in the dashboard under Developer → API keys. Keys start with or_live_ and are shown once. Send the key as a Bearer token (or in an X-API-Key header). Keep keys server-side.

curl https://otproute.com/api/v1/balance \
  -H "Authorization: Bearer or_live_xxxxxxxxxxxxxxxxxxxx"

Errors

Errors return a non-2xx status and a JSON body with a stable code and a human-readable message.

{
  "error": { "code": "insufficient_balance", "message": "Insufficient balance: this activation costs $0.20" },
  "detail": "Insufficient balance: this activation costs $0.20"
}
StatuscodeMeaning
400invalid_requestMissing or invalid parameters.
401unauthorizedMissing, revoked or invalid API key.
402insufficient_balanceWallet balance is lower than the price. Top up and retry.
404not_foundUnknown resource, or the service is not offered in that country right now.
409conflictNo numbers available, price moved above your max_price, or the action is not allowed in the current status (e.g. cancel during cooldown).
423account_lockedThe account is locked. Contact support.
429rate_limitedToo many requests. Honour the Retry-After header.
5xxerrorUpstream or internal error. Safe to retry reads; for creates, list recent activations before retrying.

Rate limits

Every account is pay as you go and gets the same limits, applied per account across all keys: 300 requests per minute, up to 10 API keys and up to 5 webhook endpoints. Activation creation is additionally protected by velocity controls. Prefer webhooks over tight polling loops — a poll every 5 seconds per open activation is plenty.

Sandbox

Sandbox accounts return simulated phone numbers (in the fictional 555 range) and deliver a simulated code after roughly 15 seconds. Wallet debits and refunds behave exactly like production, so you can test billing logic end to end. Responses include "sandbox": true. Ask support to enable sandbox on a development account.

Balance

GET/v1/balance

{ "object": "balance", "balance": …, "balance_cents": …, "currency": "USD", "formatted": "…" }

Services

GET/v1/services

All services (apps/sites) you can receive codes for. Use the id when ordering.

{ "object": "list", "data": [ { "id": "telegram", "name": "Telegram" }, { "id": "google", "name": "Google" } ] }

Countries

GET/v1/countries

{ "object": "list", "data": [ { "id": "us", "name": "United States", "dial": "+1" } ] }

Prices

GET/v1/prices?country={country}[&service={service}]

Live price per code and current availability for a service and country. Codes start from $0.20; the dashboard shows the same live prices. Without service returns every service offered in the country.

{ "object": "price", "service": "telegram", "country": "us", "price": 0.2, "price_cents": 20, "currency": "USD", "available": 118 }

Activations

An activation rents a temporary verification number for one service and waits for the code. You only pay if the code arrives: the price is held from your wallet when you create it, and if no SMS arrives before expires_at (about 20 minutes) or you cancel, the full price is refunded to your wallet automatically.

statusMeaningBilling
pendingNumber issued, waiting for the SMS.Charged (held)
receivedCode arrived — see code and sms_text.Final
completedYou marked the activation done.Final
expiredWindow closed without an SMS.Refunded
cancelledCancelled by you before a code arrived.Refunded
failedThe route could not issue a number.Refunded
{
  "id": "act_3f9c1e0b7a2d4c5e8f10",
  "object": "activation",
  "status": "received",
  "service": "telegram", "service_name": "Telegram",
  "country": "us", "country_name": "United States",
  "phone_number": "+15550100123", "number": "+15550100123",
  "code": "482913",
  "sms_text": "Telegram code: 482913",
  "price": 0.2, "price_cents": 20,
  "sandbox": false, "source": "api",
  "created_at": 1793338200, "expires_at": 1793339400,
  "received_at": 1793338241, "cancelable_after": 1793338320
}

POST/v1/activations

Body: service (required), country (required), max_price (optional, USD — reject if the live price is higher). Returns 201 with the activation.

curl -X POST https://otproute.com/api/v1/activations \
  -H "Authorization: Bearer $OTPROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"service":"telegram","country":"us","max_price":0.50}'

GET/v1/activations/{id}

Returns the activation object.

GET/v1/activations?status=&limit=

Most recent first. limit 1–200 (default 50). Filter by any status.

POST/v1/activations/{id}/cancel

Cancels a pending activation and refunds it. Routes enforce a short cooldown after issue — before cancelable_after you'll get 409. If a code arrived in the meantime, the activation becomes received instead and is not refunded.

POST/v1/activations/{id}/complete

Marks a received activation as completed and releases the number back to the route.

Dedicated numbers

Dedicated US/CA numbers start from $1.95/month and are rented and renewed from the dashboard, paid from your wallet. The API exposes them for messaging automation; renew_price is the USD amount of the next renewal.

GET/v1/numbers

{ "object": "list", "data": [ { "id": "num_8a1f…", "object": "number", "number": "+14155550142", "phone_number": "+14155550142",
  "country": "US", "status": "active", "term": "month12", "renew_price": …, "renews_at": 1801114200, "auto_renew": true } ] }

GET/v1/numbers/{id}/messages

Latest 100 inbound and outbound messages for the number.

POST/v1/numbers/{id}/messages

Body: to (E.164), text. Billed per 153-character segment from your wallet at the rate shown in the dashboard; failed sends are refunded.

{ "id": "msg_51c…", "object": "message", "status": "sent", "to": "+14155550199", "text": "Your ticket #4821 is resolved.", "price": … }

Webhooks

Register HTTPS endpoints under Developer → Webhooks, choose events (or all), and use Send test to verify your handler. Every event has the same envelope:

{
  "id": "evt_9b7d0c…",
  "type": "activation.sms_received",
  "created": 1793338241,
  "data": { …activation object… }
}
Eventdata
activation.sms_receivedActivation object with code and sms_text
activation.expiredActivation object (refunded)
activation.cancelledActivation object (refunded)
number.sms_received{id, number_id, number, to, from, text, received_at}
number.renewed / number.releasedNumber object

Verifying signatures

Each request carries X-OTPRoute-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" keyed with the endpoint's signing secret (whsec_…). Compare in constant time and reject timestamps older than 5 minutes. Also sent: X-OTPRoute-Event and X-OTPRoute-Delivery (unique — use it to de-duplicate).

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = int(parts["t"]), parts["v1"]
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Delivery & retries

Respond with any 2xx within 10 seconds. Failed deliveries are retried with backoff (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 6 h). Redirects are not followed. Endpoints must resolve to public IP addresses. The delivery log in the dashboard shows status codes and the last error for every attempt.

Next: the full integration walkthrough

Error handling, idempotency and production checklists.