Install — there is no smsroute SDK (and that's the point)
The philosophy behind smsroute's approach is deliberate. By not shipping an official Python SDK, we remove a layer of indirection between your code and the HTTP specification. The smsroute API is straightforward: it's JSON over HTTPS with Bearer token authentication. Any HTTP client that supports these features will work. Most Python applications already have requests installed for other integrations; we simply reuse that.
The benefit is freedom. Your code will never be blocked by an SDK release cycle, security patches, or dependency conflicts. You choose your HTTP client based on your architecture: requests for traditional synchronous Flask or Django apps, aiohttp for FastAPI or asyncio-native services, or even httpx for modern typed async support. All three speak the same API shape.
To get started, install your HTTP client of choice:
# For synchronous (blocking) calls with requests
pip install requests
# For asynchronous concurrency with aiohttp
pip install aiohttp
# Or modern async with httpx
pip install httpx
That's it. No pip install smsroute, no extra dependencies. Your existing deployment process and Docker images don't change. The API base URL is https://api.smsroute.cc/v1 — remember this for all requests.
Authentication and minimal send
Every request to smsroute must include your API key as a Bearer token in the Authorization header. You'll get two types of keys from the dashboard: sk_test_* for sandbox (test mode, no real SMS sent, no billing) and sk_live_* for production (real sends, real charges). Store these in environment variables, never hardcode them.
Here's the ten-line minimal example using requests:
import requests
import os
import json
api_key = os.getenv("SMSROUTE_API_KEY")
headers = {"Authorization": f"Bearer {api_key}"}
payload = {
"to": "+15551234567",
"body": "Hello from smsroute!"
}
response = requests.post("https://api.smsroute.cc/v1/messages", json=payload, headers=headers)
print(response.json())
The response, on success (HTTP 200), looks like this:
{
"id": "msg_1a2b3c4d5e6f7g8h",
"to": "+15551234567",
"body": "Hello from smsroute!",
"status": "queued",
"created_at": "2024-01-15T10:30:00Z",
"credits_used": 1
}
The id field is your message identifier — store it if you need to correlate delivery receipts or retries. The credits_used field tells you how many SMS units were consumed (usually 1, but long messages or special characters may use more). A common gotcha: don't assume the response means the SMS was delivered to the end user. It means smsroute has accepted it and queued it for sending. Delivery happens asynchronously; use webhooks to track actual delivery status.
To test with sandbox mode, swap your key to sk_test_*. Sandbox mode doesn't charge your account and doesn't send real SMS, but it does process your requests and fire webhooks just like production, allowing you to test your entire flow without costs.
Batch sending with /v1/messages/bulk
If you're sending to many recipients at once — a marketing blast, OTP broadcast, or alert distribution — don't loop and POST individually. That's inefficient and slow. Instead, use the /v1/messages/bulk endpoint, which accepts up to 1,000 messages in a single request. The API processes them in parallel on the server side, and you get back a list of message IDs in the same order as your input. This is dramatically faster for volumes above 10 recipients.
The bulk endpoint expects a JSON object with a messages array. Each message object has the same fields as a single send: to, body, and optionally from, idempotency_key, and custom metadata. Here's a practical example:
import requests
import os
api_key = os.getenv("SMSROUTE_API_KEY")
headers = {"Authorization": f"Bearer {api_key}"}
recipients = [
"+15551234567",
"+15559876543",
"+15552468135",
"+15551357924"
]
messages = [
{"to": phone, "body": "Your verification code is 123456"}
for phone in recipients
]
payload = {"messages": messages}
response = requests.post(
"https://api.smsroute.cc/v1/messages/bulk",
json=payload,
headers=headers
)
result = response.json()
print(f"Sent {len(result['ids'])} messages")
for msg_id in result['ids']:
print(f" {msg_id}")
The response includes an ids array with one entry per input message, in order:
{
"ids": [
"msg_aaa111",
"msg_bbb222",
"msg_ccc333",
"msg_ddd444"
],
"count": 4
}
You can now zip the input recipients with the returned IDs to log or store them for future webhook correlation. If you have more than 1,000 messages, chunk your list and make multiple requests. For example, a 5,000-recipient campaign would be 5 separate bulk calls. A common gotcha in bulk sending is forgetting to check the HTTP status code before parsing the response. If any validation fails (malformed phone number, missing body), the entire request returns 400 Bad Request. Always wrap in try-except and inspect the error response to identify which message caused the problem.
Error handling and retries
The smsroute API uses standard HTTP status codes to signal success or failure. Understanding these codes is essential for writing robust integrations. Different codes require different handling strategies. A 400 Bad Request means your input was invalid and will never succeed; retrying makes no sense. A 429 Too Many Requests means you've hit the rate limit; you should back off exponentially. A 5xx error means smsroute's infrastructure had a hiccup; retry with backoff. Here's how to handle all three categories:
import requests
import os
import time
from typing import Optional
def send_sms_with_retries(
to: str,
body: str,
max_retries: int = 3,
backoff_factor: float = 2.0
) -> Optional[str]:
"""Send SMS with exponential backoff retry logic."""
api_key = os.getenv("SMSROUTE_API_KEY")
headers = {"Authorization": f"Bearer {api_key}"}
payload = {"to": to, "body": body}
url = "https://api.smsroute.cc/v1/messages"
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
# 2xx: Success
if response.status_code == 200:
return response.json()["id"]
# 400: Permanent failure (bad input)
elif response.status_code == 400:
print(f"Validation error: {response.json()['error']}")
return None
# 401: Authentication failed (bad key)
elif response.status_code == 401:
print("Authentication failed. Check SMSROUTE_API_KEY.")
return None
# 402: Insufficient balance
elif response.status_code == 402:
print("Insufficient balance. Top up your account.")
return None
# 429: Rate limited (respect Retry-After)
elif response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
# 5xx: Server error (retry with backoff)
elif 500 <= response.status_code < 600:
if attempt < max_retries - 1:
wait = backoff_factor ** attempt
print(f"Server error. Retrying in {wait}s...")
time.sleep(wait)
continue
else:
print("Max retries exceeded on server error.")
return None
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
wait = backoff_factor ** attempt
print(f"Timeout. Retrying in {wait}s...")
time.sleep(wait)
else:
print("Max retries exceeded on timeout.")
return None
except Exception as e:
print(f"Unexpected error: {e}")
return None
return None
# Usage
msg_id = send_sms_with_retries("+15551234567", "Hello!")
if msg_id:
print(f"Sent message {msg_id}")
Notice the three-tiered approach: permanent failures (4xx) fail fast, rate limits (429) respect the Retry-After header, and transient errors (5xx) retry with exponential backoff. Also note the timeout parameter — always set a reasonable timeout (10 seconds is sensible) to prevent your code from hanging indefinitely waiting for a response.
Async sending with aiohttp
For high-volume or performance-critical applications, synchronous blocking calls become a bottleneck. If you're building a FastAPI service that receives 1,000 webhook calls per second and each one triggers an SMS, you can't afford to block your event loop waiting for HTTP responses. This is where aiohttp shines. It allows you to send hundreds of SMS concurrently without creating separate threads or processes.
The pattern is simple: create a single aiohttp.ClientSession (usually once at startup, reused for the lifetime of your app), then use asyncio.gather() to fire multiple requests concurrently. Here's a practical example:
import aiohttp
import asyncio
import os
from typing import List, Dict
class SMSClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.smsroute.cc/v1"
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, *args):
if self.session:
await self.session.close()
async def send_sms(self, to: str, body: str) -> Dict:
"""Send a single SMS asynchronously."""
headers = {"Authorization": f"Bearer {self.api_key}"}
payload = {"to": to, "body": body}
async with self.session.post(
f"{self.base_url}/messages",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)
) as resp:
return await resp.json()
async def send_bulk(self, recipients: List[str], body: str) -> List[Dict]:
"""Send SMS to multiple recipients concurrently."""
tasks = [self.send_sms(to, body) for to in recipients]
return await asyncio.gather(*tasks, return_exceptions=True)
# Usage example
async def main():
api_key = os.getenv("SMSROUTE_API_KEY")
recipients = ["+15551234567", "+15559876543", "+15552468135"]
async with SMSClient(api_key) as client:
results = await client.send_bulk(recipients, "Your code: 123456")
for to, result in zip(recipients, results):
if isinstance(result, dict) and "id" in result:
print(f"{to}: {result['id']}")
else:
print(f"{to}: Error — {result}")
asyncio.run(main())
This pattern uses Python's async context manager protocol (async with) to ensure the session is properly created and closed. The send_bulk() method uses asyncio.gather() to send all SMS concurrently. Even with 1,000 recipients, this completes in roughly the time of a single round-trip to the API (plus server processing), not 1,000x the round-trip time. For production applications, you'd typically instantiate the SMSClient once at app startup and keep it alive for the app's lifetime, then pass it to request handlers as a dependency (in FastAPI, use Depends()).
Webhook ingestion — FastAPI + HMAC verification
Sending SMS is only half the story. The real value emerges when you receive delivery notifications via webhooks. smsroute fires a webhook event to your endpoint whenever an SMS status changes: queued → sent, sent → delivered, sent → failed, etc. These webhooks allow you to track which SMS succeeded, which failed, and which are stuck. You can then automate follow-up: retry failed sends, log analytics, trigger alerts, or update your database.
However, webhooks introduce a security concern: how do you know the request actually came from smsroute and not an attacker? smsroute uses HMAC-SHA256 signatures. Every webhook includes a timestamp and signature in the request header. You reconstruct the signed payload using your API key and compare it with a timing-safe comparison function. If they match, the webhook is authentic.
Here's a production-ready FastAPI webhook receiver:
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import json
import os
from datetime import datetime, timedelta
app = FastAPI()
API_KEY = os.getenv("SMSROUTE_API_KEY")
WEBHOOK_TOLERANCE_SECONDS = 300 # Reject webhooks older than 5 minutes
@app.post("/webhooks/sms")
async def handle_sms_webhook(request: Request):
"""Receive and verify smsroute delivery webhooks."""
# Read the raw request body (required for signature verification)
body = await request.body()
# Extract signature header: format is "t=,v1="
signature_header = request.headers.get("x-smsroute-signature", "")
if not signature_header:
raise HTTPException(status_code=403, detail="Missing signature header")
# Parse timestamp and signature
parts = signature_header.split(",")
timestamp_str = None
signature = None
for part in parts:
if part.startswith("t="):
timestamp_str = part[2:]
elif part.startswith("v1="):
signature = part[3:]
if not timestamp_str or not signature:
raise HTTPException(status_code=403, detail="Malformed signature header")
# Verify timestamp is recent (prevent replay attacks)
try:
timestamp = int(timestamp_str)
now = int(datetime.utcnow().timestamp())
if abs(now - timestamp) > WEBHOOK_TOLERANCE_SECONDS:
raise HTTPException(status_code=403, detail="Timestamp too old")
except ValueError:
raise HTTPException(status_code=403, detail="Invalid timestamp")
# Reconstruct the signed content: timestamp.body
signed_content = f"{timestamp_str}.{body.decode('utf-8')}"
# Compute HMAC-SHA256 using your API key
expected_signature = hmac.new(
API_KEY.encode("utf-8"),
signed_content.encode("utf-8"),
hashlib.sha256
).hexdigest()
# Compare signatures using timing-safe comparison
if not hmac.compare_digest(expected_signature, signature):
raise HTTPException(status_code=403, detail="Invalid signature")
# Signature verified. Process the webhook.
payload = json.loads(body)
print(f"Webhook received: {payload['type']}")
print(f" Message ID: {payload['message_id']}")
print(f" Status: {payload['status']}")
# Example: update your database with delivery status
# await db.update_message_status(payload['message_id'], payload['status'])
return {"ok": True}
Key security considerations: always use hmac.compare_digest() instead of == for signature comparison — this is timing-safe and prevents side-channel attacks. Always verify the timestamp is recent (within 5 minutes) to reject replayed webhooks. Parse the signature header carefully; the format is strict. And remember that the body must be read as raw bytes before parsing JSON, because the signature is computed over the exact byte sequence sent by the server.
The webhook payload structure varies by event type, but always includes message_id, status, and timestamp. Common statuses are: queued (accepted, pending send), sent (carrier accepted), delivered (reached the recipient's device), failed (permanent failure, e.g., invalid number), and undelivered (temporary failure, will retry). Use these statuses to drive your application logic — for example, mark OTP as "confirmed" only on delivered, and retry undelivered messages after a delay.
Idempotency keys and delivery receipts
Imagine your code sends an SMS, but the network breaks before you receive the response. You don't know if the SMS was actually sent or if it was lost in transit. Your natural instinct is to retry. But if you just retry the same request, and the SMS did go through the first time, you've now sent the same message twice — a duplicate that annoys the recipient and wastes credits.
smsroute solves this with idempotency keys. When you send an SMS, include a unique Idempotency-Key header (typically a UUIDv4). If the request fails and you retry with the same key, smsroute returns the same message ID and doesn't charge you again. The deduplication window is 24 hours, so you can safely retry within that window. Here's how to use it:
import requests
import os
import uuid
def send_sms_idempotent(to: str, body: str) -> str:
"""Send SMS with automatic deduplication via idempotency key."""
api_key = os.getenv("SMSROUTE_API_KEY")
idempotency_key = str(uuid.uuid4())
headers = {
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": idempotency_key
}
payload = {"to": to, "body": body}
response = requests.post(
"https://api.smsroute.cc/v1/messages",
json=payload,
headers=headers
)
result = response.json()
message_id = result["id"]
# Log the idempotency key for future reference
print(f"Message {message_id} with idempotency key {idempotency_key}")
return message_id
# Safe retry: same idempotency key returns same message ID
msg_id = send_sms_idempotent("+15551234567", "Hello")
# If network fails and you retry:
msg_id_retry = send_sms_idempotent("+15551234567", "Hello")
# Both calls return the same message ID; only one SMS is sent.
In practice, you'd store the idempotency key alongside your message record in the database. If a retry is needed hours later, you can look it up and include it in the retry request. This is especially important in financial transactions (like charging a user) where exactly-once semantics are non-negotiable.
For tracking delivery at scale, combine idempotency keys with webhook delivery notifications. When a webhook fires, it includes the message ID. Match that against your database to update the delivery status. If you never receive a webhook for a message, it may have failed silently; after a reasonable timeout (e.g., 30 minutes), query the message status endpoint to check if it's still pending, or mark it as undelivered and retry with a fresh idempotency key.
Testing with sandbox mode and pytest
Sandbox mode (sk_test_* keys) is your testing environment. It processes requests identically to production but doesn't actually send SMS, doesn't charge your account, and fires webhooks to your test endpoint. The magic phone numbers are:
+15005550001— always delivers successfully+15005550002— always fails permanently+15005550003— always marked undelivered (temporary failure)
Use these in pytest fixtures to test success and failure paths:
import pytest
import os
from unittest.mock import patch
import requests
@pytest.fixture
def sandbox_api_key():
"""Provide sandbox API key for tests."""
key = os.getenv("SMSROUTE_TEST_KEY", "sk_test_dummy_key_for_testing")
return key
@pytest.fixture
def sms_client(sandbox_api_key):
"""Fixture that yields a reusable SMS client."""
class Client:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.smsroute.cc/v1"
def send(self, to: str, body: str) -> dict:
headers = {"Authorization": f"Bearer {self.api_key}"}
response = requests.post(
f"{self.base_url}/messages",
json={"to": to, "body": body},
headers=headers
)
return response.json()
return Client(sandbox_api_key)
def test_sms_delivery_success(sms_client):
"""Test successful SMS delivery."""
result = sms_client.send("+15005550001", "Test message")
assert "id" in result
assert result["status"] == "queued"
def test_sms_delivery_failure(sms_client):
"""Test permanent SMS failure."""
result = sms_client.send("+15005550002", "Test message")
# Sandbox mode returns id but webhook will mark as failed
assert "id" in result
def test_sms_undelivered(sms_client):
"""Test undelivered (retry-able) status."""
result = sms_client.send("+15005550003", "Test message")
assert "id" in result
# Webhook will eventually mark as undelivered
def test_bulk_send(sms_client):
"""Test bulk send with multiple recipients."""
recipients = ["+15005550001", "+15005550001", "+15005550002"]
headers = {"Authorization": f"Bearer {sms_client.api_key}"}
payload = {
"messages": [
{"to": phone, "body": "Bulk test"}
for phone in recipients
]
}
response = requests.post(
f"{sms_client.base_url}/messages/bulk",
json=payload,
headers=headers
)
result = response.json()
assert len(result["ids"]) == 3
Notice the fixture pattern: @pytest.fixture creates reusable test components. The sms_client fixture depends on sandbox_api_key, demonstrating how to compose fixtures. In your CI/CD pipeline, set the environment variable SMSROUTE_TEST_KEY to a sandbox key, and all tests will run against the test API without hitting production.
Frequently asked questions
- Why doesn't smsroute have an official Python SDK?
- smsroute intentionally avoids SDKs to keep integrations lightweight and flexible. The HTTP API is simple enough that requests library (or httpx, or aiohttp) is the entire client. This eliminates version lock-in and lets you upgrade smsroute without waiting for SDK releases.
- Can I use httpx instead of requests?
- Yes, absolutely. httpx is drop-in compatible with requests for sync calls and also supports async. Any HTTP library that speaks Bearer token authentication will work with smsroute.
- What's the difference between sync and async sending?
- Sync (requests) blocks your code until the API responds, suitable for small volumes or when you don't need concurrency. Async (aiohttp) allows you to send hundreds of SMS concurrently in a single event loop, ideal for bulk campaigns or high-throughput integrations. For a FastAPI app receiving high QPS, async is essential.
- How do I verify webhook signatures from smsroute?
-
smsroute signs webhooks with HMAC-SHA256 using your API key. Extract the timestamp and signature from the request header (format:
t=<timestamp>,v1=<signature>), reconstruct the signed content astimestamp.body, compute the expected HMAC using your key, and usehmac.compare_digest()for a timing-safe comparison. This prevents spoofed webhooks. - What happens if my API key is compromised?
-
Rotate your key immediately via the smsroute dashboard. Since no KYC is required and payments are crypto, compromise is detected by unexpected SMS activity. Use separate
sk_test_*keys for development andsk_live_*for production to limit blast radius. - Can I batch 10,000 SMS in one call?
-
The
/v1/messages/bulkendpoint accepts up to 1,000 messages per request. For larger volumes, chunk your list into 1,000-message batches and POST each chunk. Responses include message IDs in the same order as your input. A simple approach: use a list comprehension to slice the recipients in 1,000-item chunks, then map each chunk to a bulk request. - How do idempotency keys prevent double-sends?
-
Set the
Idempotency-Keyheader to a UUIDv4. If your code retries the same request within 24 hours, smsroute returns the same message ID and does not charge you twice. This is critical for unreliable networks or when retry logic is uncertain. - What are the sandbox magic phone numbers?
-
Use
sk_test_*keys with+15005550001for delivered,+15005550002for failed, or+15005550003for undelivered. These always succeed without actually sending SMS, letting you test webhook delivery and error flows in CI/CD without costs.
Related pages
For more information on smsroute, explore these resources:
- API reference documentation — Complete specification of all endpoints, request/response formats, and error codes.
- Pricing and countries — Rate card across 149 countries and payment methods.
- Node.js quickstart — Equivalent integration guide for JavaScript/TypeScript.
- Migrate from Twilio — Step-by-step guide for moving existing Twilio integrations to smsroute.
- Two-factor authentication (2FA) — Best practices for OTP-based account security.
- One-time passwords (OTP) — Generating and verifying OTPs with smsroute webhooks.
Why doesn't smsroute have an official Python SDK?
smsroute intentionally avoids SDKs to keep integrations lightweight and flexible. The HTTP API is simple enough that requests library (or httpx, or aiohttp) is the entire client. This eliminates version lock-in and lets you upgrade smsroute without waiting for SDK releases.
Can I use httpx instead of requests?
Yes, absolutely. httpx is drop-in compatible with requests for sync calls and also supports async. Any HTTP library that speaks Bearer token authentication will work with smsroute.
What's the difference between sync and async sending?
Sync (requests) blocks your code until the API responds, suitable for small volumes or when you don't need concurrency. Async (aiohttp) allows you to send hundreds of SMS concurrently in a single event loop, ideal for bulk campaigns or high-throughput integrations.
How do I verify webhook signatures from smsroute?
smsroute signs webhooks with HMAC-SHA256 using your API key. Extract the timestamp and signature from the request header, reconstruct the signed content, and use hmac.compare_digest() for a timing-safe comparison. This prevents spoofed webhooks.
What happens if my API key is compromised?
Rotate your key immediately via the smsroute dashboard. Since no KYC is required and payments are crypto, compromise is detected by unexpected SMS activity. Use separate sk_test_* keys for development and sk_live_* for production to limit blast radius.
Can I batch 10,000 SMS in one call?
The /v1/messages/bulk endpoint accepts up to 1,000 messages per request. For larger volumes, chunk your list into 1,000-message batches and POST each chunk. Responses include message IDs in the same order as your input.
How do idempotency keys prevent double-sends?
Set the Idempotency-Key header to a UUIDv4. If your code retries the same request within 24 hours, smsroute returns the same message ID and does not charge you twice. This is critical for unreliable networks or when retry logic is uncertain.
What are the sandbox magic phone numbers?
Use sk_test_* keys with +15005550001 for delivered, +15005550002 for failed, or +15005550003 for undelivered. These always succeed without actually sending SMS, letting you test webhook delivery and error flows.
Why doesn't smsroute have an official Python SDK?
smsroute intentionally avoids SDKs to keep integrations lightweight and flexible. The HTTP API is simple enough that requests library (or httpx, or aiohttp) is the entire client. This eliminates version lock-in and lets you upgrade smsroute without waiting for SDK releases.
Can I use httpx instead of requests?
Yes, absolutely. httpx is drop-in compatible with requests for sync calls and also supports async. Any HTTP library that speaks Bearer token authentication will work with smsroute.
What's the difference between sync and async sending?
Sync (requests) blocks your code until the API responds, suitable for small volumes or when you don't need concurrency. Async (aiohttp) allows you to send hundreds of SMS concurrently in a single event loop, ideal for bulk campaigns or high-throughput integrations.
How do I verify webhook signatures from smsroute?
smsroute signs webhooks with HMAC-SHA256 using your API key. Extract the timestamp and signature from the request header, reconstruct the signed content, and use hmac.compare_digest() for a timing-safe comparison. This prevents spoofed webhooks.
What happens if my API key is compromised?
Rotate your key immediately via the smsroute dashboard. Since no KYC is required and payments are crypto, compromise is detected by unexpected SMS activity. Use separate sk_test_* keys for development and sk_live_* for production to limit blast radius.
Can I batch 10,000 SMS in one call?
The /v1/messages/bulk endpoint accepts up to 1,000 messages per request. For larger volumes, chunk your list into 1,000-message batches and POST each chunk. Responses include message IDs in the same order as your input.
How do idempotency keys prevent double-sends?
Set the Idempotency-Key header to a UUIDv4. If your code retries the same request within 24 hours, smsroute returns the same message ID and does not charge you twice. This is critical for unreliable networks or when retry logic is uncertain.
What are the sandbox magic phone numbers?
Use sk_test_* keys with +15005550001 for delivered, +15005550002 for failed, or +15005550003 for undelivered. These always succeed without actually sending SMS, letting you test webhook delivery and error flows.
Why doesn't smsroute have an official Python SDK?
smsroute intentionally avoids SDKs to keep integrations lightweight and flexible. The HTTP API is simple enough that requests library (or httpx, or aiohttp) is the entire client. This eliminates version lock-in and lets you upgrade smsroute without waiting for SDK releases.
Can I use httpx instead of requests?
Yes, absolutely. httpx is drop-in compatible with requests for sync calls and also supports async. Any HTTP library that speaks Bearer token authentication will work with smsroute.
What's the difference between sync and async sending?
Sync (requests) blocks your code until the API responds, suitable for small volumes or when you don't need concurrency. Async (aiohttp) allows you to send hundreds of SMS concurrently in a single event loop, ideal for bulk campaigns or high-throughput integrations.
How do I verify webhook signatures from smsroute?
smsroute signs webhooks with HMAC-SHA256 using your API key. Extract the timestamp and signature from the request header, reconstruct the signed content, and use hmac.compare_digest() for a timing-safe comparison. This prevents spoofed webhooks.
What happens if my API key is compromised?
Rotate your key immediately via the smsroute dashboard. Since no KYC is required and payments are crypto, compromise is detected by unexpected SMS activity. Use separate sk_test_* keys for development and sk_live_* for production to limit blast radius.
Can I batch 10,000 SMS in one call?
The /v1/messages/bulk endpoint accepts up to 1,000 messages per request. For larger volumes, chunk your list into 1,000-message batches and POST each chunk. Responses include message IDs in the same order as your input.
How do idempotency keys prevent double-sends?
Set the Idempotency-Key header to a UUIDv4. If your code retries the same request within 24 hours, smsroute returns the same message ID and does not charge you twice. This is critical for unreliable networks or when retry logic is uncertain.
What are the sandbox magic phone numbers?
Use sk_test_* keys with +15005550001 for delivered, +15005550002 for failed, or +15005550003 for undelivered. These always succeed without actually sending SMS, letting you test webhook delivery and error flows.
Why doesn't smsroute have an official Python SDK?
smsroute intentionally avoids SDKs to keep integrations lightweight and flexible. The HTTP API is simple enough that requests library (or httpx, or aiohttp) is the entire client. This eliminates version lock-in and lets you upgrade smsroute without waiting for SDK releases.
Can I use httpx instead of requests?
Yes, absolutely. httpx is drop-in compatible with requests for sync calls and also supports async. Any HTTP library that speaks Bearer token authentication will work with smsroute.
What's the difference between sync and async sending?
Sync (requests) blocks your code until the API responds, suitable for small volumes or when you don't need concurrency. Async (aiohttp) allows you to send hundreds of SMS concurrently in a single event loop, ideal for bulk campaigns or high-throughput integrations.
How do I verify webhook signatures from smsroute?
smsroute signs webhooks with HMAC-SHA256 using your API key. Extract the timestamp and signature from the request header, reconstruct the signed content, and use hmac.compare_digest() for a timing-safe comparison. This prevents spoofed webhooks.
What happens if my API key is compromised?
Rotate your key immediately via the smsroute dashboard. Since no KYC is required and payments are crypto, compromise is detected by unexpected SMS activity. Use separate sk_test_* keys for development and sk_live_* for production to limit blast radius.
Can I batch 10,000 SMS in one call?
The /v1/messages/bulk endpoint accepts up to 1,000 messages per request. For larger volumes, chunk your list into 1,000-message batches and POST each chunk. Responses include message IDs in the same order as your input.
How do idempotency keys prevent double-sends?
Set the Idempotency-Key header to a UUIDv4. If your code retries the same request within 24 hours, smsroute returns the same message ID and does not charge you twice. This is critical for unreliable networks or when retry logic is uncertain.
What are the sandbox magic phone numbers?
Use sk_test_* keys with +15005550001 for delivered, +15005550002 for failed, or +15005550003 for undelivered. These always succeed without actually sending SMS, letting you test webhook delivery and error flows.