Overview
smsdorais a device-based SMS gateway that lets you send SMS messages through real Android devices. Instead of relying on expensive SMS provider APIs, you use your own phone(s) as the sending infrastructure — paying only your carrier's standard SMS rates.
Here's the idea in a nutshell:
- You install our Android app on a phone and keep it connected.
- You create an API key from the dashboard.
- Your application calls our API endpoint with the recipient number and message.
- The backend dispatches the SMS job to your phone, which sends it natively and reports the delivery status back.
Getting Started
Send your first SMS in under 5 minutes:
Create an Account
Sign up at smsfor.me/signup with your email or Google account.
Connect Your Android Phone
Install the smsdora app from the Play Store. Log in with your account and grant SMS permissions. Your device will appear in the dashboard automatically.
Create an API Key
Go to Dashboard → API Keys and create a new key. Make sure the messages:send scope is enabled. Copy and save the key — it's only shown once.
Send Your First SMS
Make an API call to our send endpoint:
curl -X POST https://api.smsdora.com/messages/send \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipient": "+2348034125567",
"message": "Hello from smsdora!"
}'API Keys
API keys are how your application authenticates with smsdora. You create them from the API Keys page in the dashboard.
Store Your Key Securely
The full API key is only shown oncewhen you create it. We store a hashed version — we can't retrieve the original for you. Treat it like a password.
How to Use Your API Key
Include the key in the request header using either method:
x-api-key: smsgw_ab12cd34_e7f8a9b0c1d2e3f4...Authorization: Bearer smsgw_ab12cd34_e7f8a9b0c1d2e3f4...Scopes
Each API key carries one or more scopes that control what it can do. Grant only what your integration needs — you choose the scopes when creating the key.
| Scope | Grants |
|---|---|
messages:send | Send new SMS and resend failed messages |
messages:read | List and read your message logs |
devices:read | List and read your connected devices |
Key Features
- Rate limiting — You can set a per-minute request limit per key
- Expiration — Optionally set an expiry date for automatic deactivation
- Revoke — Instantly deactivate a key if compromised
- Rotate — Generate a new secret while keeping the same key settings
Never use API keys in client-side code
Don't embed your API key in mobile apps, browser JavaScript, or any public-facing code. Always make API calls from your backend server to keep the key secret.
Sending SMS
Send an SMS with a single POST request:
https://api.smsdora.com/messages/sendHeaders
| Header | Value |
|---|---|
x-api-key | Your API key |
Content-Type | application/json |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
recipient | string | Yes | Phone number in E.164 format (e.g. +2348034125567) |
message | string | Yes | SMS text content (1–1600 characters) |
Example Request
curl -X POST https://api.smsdora.com/messages/send \
-H "x-api-key: smsgw_ab12cd34_e7f8a9b0c1d2e3f4..." \
-H "Content-Type: application/json" \
-d '{
"recipient": "+212600000000",
"message": "Your verification code is 4829"
}'Success Response
{
"statusCode": 201,
"message": "SMS queued for delivery",
"data": {
"id": "64a...",
"recipient": "+212600000000",
"message": "Your verification code is 4829",
"status": "dispatching",
"device": {
"id": "64b...",
"deviceName": "My Samsung S24"
}
}
}Automatic device selection
You don't need to specify which phone sends the SMS. The backend automatically picks the best available device from your account — one that is online, active, and most recently seen.
Message Status
After sending, your message progresses through these statuses:
| Status | What it means |
|---|---|
| queued | Message received by our backend |
| dispatching | Job sent to your Android device via push notification |
| sending | Your phone is actively sending the SMS |
| sent | SMS handed off to the carrier network |
| delivered | Carrier confirmed delivery to the recipient |
| failed | SMS could not be sent or delivered |
You can track the status of any message from the Messages page in the dashboard — or fetch it programmatically with the Reading Messages endpoints below.
Reading Messages
Pull your message history programmatically to track delivery, reconcile orders, or build your own reporting. These endpoints require an API key with the messages:read scope. Results are always scoped to the key's owner.
https://api.smsdora.com/messagesQuery Parameters
All parameters are optional. Results are sorted newest first.
| Parameter | Description |
|---|---|
status | Filter by status: queued, dispatching, sending, sent, delivered, failed |
recipient | Partial, case-insensitive match on the recipient number |
deviceId | Only messages sent from this device |
source | api, dashboard, manual, system, or campaign |
from / to | ISO date range filter on creation time |
page | Page number (default 1) |
limit | Results per page (default 20, max 100) |
Example Request
curl "https://api.smsdora.com/messages?status=failed&limit=20" \
-H "x-api-key: YOUR_API_KEY"Success Response
{
"statusCode": 200,
"data": {
"messages": [ /* array of message objects (see below) */ ],
"pagination": {
"total": 42,
"page": 1,
"limit": 20,
"totalPages": 3
}
}
}Get a Single Message
https://api.smsdora.com/messages/:id{
"statusCode": 200,
"data": {
"id": "64a...",
"deviceId": "64b...",
"campaignId": null,
"resentFromMessageId": null,
"resentToMessageId": null,
"recipient": "+212600000000",
"message": "Your verification code is 4829",
"segmentsCount": 1,
"status": "delivered",
"failureReason": null,
"provider": "android_device",
"source": "api",
"externalRequestId": "order-123",
"queuedAt": "2026-06-16T10:00:00.000Z",
"dispatchedAt": "2026-06-16T10:00:01.000Z",
"sendingAt": "2026-06-16T10:00:02.000Z",
"sentAt": "2026-06-16T10:00:03.000Z",
"deliveredAt": "2026-06-16T10:00:09.000Z",
"failedAt": null,
"createdAt": "2026-06-16T10:00:00.000Z",
"updatedAt": "2026-06-16T10:00:09.000Z"
}
}Message Object Fields
| Field | Description |
|---|---|
id | Unique message identifier |
recipient | Destination phone number |
message | SMS text content |
segmentsCount | Number of SMS segments the text was split into |
status | Current delivery status |
failureReason | Why the message failed, when applicable |
deviceId | Device that sent (or will send) the message |
source | Origin of the message (api, dashboard, etc.) |
externalRequestId | Your own correlation id, if provided when sending |
resentFromMessageId | On a resent message — points to the original failed message |
resentToMessageId | On an original message once resent — points to the new message |
*At timestamps | queuedAt, dispatchedAt, sendingAt, sentAt, deliveredAt, failedAt, plus createdAt / updatedAt |
Resend a Failed SMS
If a message fails to send, you can resend it without rebuilding the request. Resending creates a brand-new message that reuses the original's recipient, text, device, and externalRequestId. Requires the messages:send scope.
https://api.smsdora.com/messages/:id/resendOnly failed messages can be resent
The message referenced by :id must currently have a failed status. Resending a queued, sent, or delivered message returns 409 Conflict.
Example Request
curl -X POST https://api.smsdora.com/messages/MESSAGE_ID/resend \
-H "x-api-key: YOUR_API_KEY"Success Response
{
"statusCode": 201,
"message": "SMS queued for delivery",
"data": {
"id": "<new message id>",
"resentFromMessageId": "<original failed message id>",
"resentToMessageId": null,
"recipient": "+212600000000",
"message": "Your verification code is 4829",
"status": "dispatching",
"source": "api",
"externalRequestId": "order-123",
"device": {
"id": "64b...",
"deviceName": "My Samsung S24",
"phoneNumber": "+212600000000"
}
}
}The original is preserved
The original failed message is left untouched except for a resentToMessageId link pointing to the new message, so you always keep a full audit trail.
Devices
List the Android devices connected to your account and check whether they are online before sending. Requires an API key with the devices:read scope.
https://api.smsdora.com/devicesExample Request
curl https://api.smsdora.com/devices \
-H "x-api-key: YOUR_API_KEY"Success Response
{
"statusCode": 200,
"data": [
{
"id": "64b...",
"deviceId": "android-abc123",
"deviceName": "My Samsung S24",
"platform": "android",
"brand": "Samsung",
"model": "SM-S921B",
"isActive": true,
"status": "online",
"batteryLevel": 87,
"isCharging": false,
"simLabel": "Orange MA",
"simSlot": 1,
"lastSeenAt": "2026-06-16T10:00:00.000Z"
}
]
}Get a Single Device
https://api.smsdora.com/devices/:idReturns fuller device metadata, including phoneNumber, androidVersion, and appVersion.
Secrets are never exposed to API clients
Device responses for API-key clients never include sensitive fields such as the device key or the push token (fcmToken). Only safe, read-only device metadata is returned.
OTP Authentication
Send and verify one-time passcodes over SMS with the official @smsdora/otp Node.js SDK. It wraps the OTP endpoints with built-in retries, idempotent sends, typed errors, and local verification-token validation. Requires an API key with the otp scope.
Server-side only
The SDK holds your secret API key — never ship it to a browser or mobile app. For client apps, use a SmsDora publishable key, which is restricted to the otp scope.
Install
npm install @smsdora/otpZero runtime dependencies (built-in fetch + node:crypto). Ships ESM and CommonJS with full TypeScript types. Requires Node.js 18 or later.
Quick Start
import { SmsdoraOtp } from '@smsdora/otp';
const otp = new SmsdoraOtp({
apiKey: process.env.SMSDORA_SECRET_KEY!,
baseUrl: 'https://api.smsdora.com',
tokenSecret: process.env.OTP_TOKEN_SECRET,
});
// 1. Send a code
const challenge = await otp.send({
recipient: '+2348031234567',
purpose: 'login',
});
// 2. Verify what the user typed
const result = await otp.verify({
challengeId: challenge.id,
code: '123456',
});
if (result.verified) {
const claims = otp.verifyToken(result.verificationToken!);
console.log('verified phone:', claims.sub, 'for', claims.purpose);
}Client Options
Pass these to new SmsdoraOtp(options):
| Option | Type | Default | Notes |
|---|---|---|---|
| apiKey | string | — | Required. Secret key with the otp scope. |
| baseUrl | string | — | Required. Your SmsDora API base URL. |
| tokenSecret | string | — | Shared OTP_TOKEN_SECRET; needed for verifyToken(). |
| timeoutMs | number | 10000 | Per-request timeout. |
| maxRetries | number | 2 | Retries on network / 5xx / 429. |
| fetch | typeof fetch | global | Custom fetch implementation. |
Sending a Code
send(params) returns a Promise<OtpChallenge>. Only recipient is required — the rest are optional.
await otp.send({
recipient: '+2348031234567',
purpose: 'login',
codeLength: 6,
ttlSeconds: 300,
templateOverride: 'Your code is {{code}} ({{ttl}} min)',
metadata: { userId: 'u_123' },
idempotencyKey: 'order-42',
});Verifying a Code
verify(params) resolves with { verified, status, attemptsRemaining, verificationToken? }. A wrong code resolves with verified: false — it does not throw. When verification succeeds, validate the returned token locally with verifyToken().
const result = await otp.verify({
challengeId: challenge.id,
code: '123456',
});
// result.verified -> boolean
// result.status -> challenge status
// result.attemptsRemaining -> number
// result.verificationToken -> string (present when verified)Other Methods
resend({ challengeId })— re-delivers a fresh code for a pending challenge. Respects the resend cooldown and cap.getStatus(challengeId)— returns the currentOtpChallenge.verifyToken(token)— validates a verification token locally (HS256, no network). RequirestokenSecret.verifyTokenRemote(token)— validates on the server with single-use enforcement.
Error Handling
Failed calls throw typed errors you can catch by class. A wrong code is not an error — only delivery, auth, rate-limit, and network problems throw.
import { RateLimitError, DeliveryError, AuthError } from '@smsdora/otp';
try {
await otp.send({ recipient });
} catch (err) {
if (err instanceof RateLimitError) {
// err.retryAfterSeconds
} else if (err instanceof DeliveryError) {
// no online device / FCM failure
} else if (err instanceof AuthError) {
// bad key / missing scope
}
}| Class | code | When |
|---|---|---|
| ConfigError | config_error | Missing apiKey / baseUrl / tokenSecret. |
| ValidationError | validation_error | 400 — bad params. |
| AuthError | auth_error | 401 / 403. |
| NotFoundError | not_found | 404. |
| RateLimitError | rate_limited | 429 — has retryAfterSeconds. |
| DeliveryError | delivery_failed | 502 — could not deliver. |
| ServerError | server_error | 5xx. |
| NetworkError | network_error | Timeout / connection failure. |
| TokenVerificationError | token_invalid | Bad / expired verification token. |
Error Handling
If something goes wrong, the API returns a JSON error with an HTTP status code:
{
"statusCode": 400,
"message": "No eligible device available",
"error": "Bad Request"
}Common Errors
| Code | Cause | What to do |
|---|---|---|
| 400 | No eligible device available | Make sure at least one device is online and active |
| 400 | Invalid phone number or message | Check the recipient format and message length |
| 401 | Invalid, expired, or revoked API key | Check your API key or create a new one |
| 403 | Rate limit exceeded | Wait and retry, or increase your key's rate limit |
| 403 | Missing required scope (e.g. messages:send, messages:read, devices:read) | Create a new key with the correct scope |
| 404 | Message or device not found (or not owned by your key) | Check the id — you can only access your own resources |
| 409 | Resending a message that is not in a failed state | Only failed messages can be resent |
Rate Limits
Each API key can have a configurable rate limit (requests per minute). You set this when creating the key in the dashboard.
- If no rate limit is set, the key has unlimited requests per minute
- Exceeding the limit returns
403 Forbidden - The rate limit resets on a rolling 60-second window
Plan-based limits
Your subscription plan also determines the monthly SMS volume and number of devices you can connect. Check the Pricing page for details.
Code Examples
Ready-to-use examples for sending an SMS. Replace YOUR_API_KEY with your actual key.
cURL
curl -X POST https://api.smsdora.com/messages/send \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipient": "+2348034125567",
"message": "Hello from smsdora!"
}'JavaScript / Node.js
const response = await fetch(
"https://api.smsdora.com/messages/send",
{
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
recipient: "+2348034125567",
message: "Hello from smsdora!",
}),
}
);
const data = await response.json();
console.log(data);Python
import requests
response = requests.post(
"https://api.smsdora.com/messages/send",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"recipient": "+2348034125567",
"message": "Hello from smsdora!",
},
)
print(response.json())PHP
<?php
$ch = curl_init("https://api.smsdora.com/messages/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"x-api-key: YOUR_API_KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"recipient" => "+2348034125567",
"message" => "Hello from smsdora!",
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;C# / .NET
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key", "YOUR_API_KEY");
var payload = new {
recipient = "+2348034125567",
message = "Hello from smsdora!"
};
var response = await client.PostAsJsonAsync(
"https://api.smsdora.com/messages/send",
payload
);
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine(result);Frequently Asked Questions
Ready to get started?
Create your free account, connect a phone, and start sending SMS in minutes.