Skip to content
SMS API

SMS API Without Twilio: Send and Receive Texts From Your Own Number

An SMS API that sends from your own number: connect an Android phone, get an API key and send texts with cURL, Python, Node.js, PHP or Go. Free plan to start.

smsportal teamUpdated: 15 min read
A laptop with code connected to a phone that sends an SMS through an API

Most SMS APIs charge per message and send from numbers you do not own, so customers cannot reply and you inherit carrier registration paperwork. There is another way: turn an Android phone with your SIM into the gateway and send and receive SMS through a REST API from your own number. This guide covers setup, working code in five languages, error handling, delivery status, scheduling, limits, and an honest take on when a wholesale provider is the better choice.

How does an SMS API based on your own phone work?

Your app sends an HTTP request to smsportal, the server hands the job to your phone, and the phone sends a normal SMS over your carrier. The recipient sees your regular number and can reply. Replies flow back through the same channel to the dashboard, the API and webhooks.

The flow:

  1. Your code calls POST /gateway/send-sms with your API key.
  2. The server accepts the job and returns a smsBatchId (accepted, not yet sent).
  3. The phone picks up the job and sends the SMS from its SIM.
  4. The phone reports back: sent, delivered or failed.

There is no per-message fee from smsportal. You pay your mobile plan (many EU and US plans include unlimited texting) plus a smsportal plan, with a free plan to start. See pricing. For a cost breakdown against per-message providers, read SMS API cost comparison.

Note: the phone is the transmitter. It needs power, signal and internet. If it is offline, queued messages wait up to 72 hours and then expire.

What do you need to get started?

You need three things and about 15 minutes:

  • an Android phone (7.0 or newer) with an active SIM, ideally kept on a charger,
  • a smsportal account (sign up, the free plan is enough),
  • any language that can make an HTTP request: cURL, Python, Node.js, PHP, Go.

Download the app directly: smsportal.apk. For the broader picture of phone-based gateways, see what an Android SMS gateway is, and the Android app features page.

Step 1: How do you connect the phone?

Install the app, grant SMS permissions and scan the QR code from the dashboard. The phone then appears as a device. This is a one-time setup.

  1. Install the APK on the phone whose SIM you want to send from.
  2. Walk through the short onboarding and allow sending and receiving SMS.
  3. In the dashboard choose Add device and scan the QR code (or enter the key manually).
  4. Name the phone, for example "Front desk".
Connecting the app to the server: server URL, API key and QR scan (Polish UI)

On the permissions screen, also exempt the app from battery optimization. Android can put background apps to sleep, which stalls sending.

Permissions screen: SMS, notifications and battery (Polish UI)

Step 2: How do you create an API key?

Create the key in the dashboard with one click. It identifies your account and can be revoked at any time. Keep it in an environment variable, never in a repository.

bash
export SMSPORTAL_API_KEY="paste_your_key_here"

Every request carries the key in the x-api-key header. The base URL is https://smsportal.app/api/v1.

smsportal web dashboard: devices, API keys and usage
smsportal web dashboard: devices, API keys and usage

Step 3: How do you send your first SMS through the API?

POST to /gateway/send-sms with recipients in international format and the message text. With one device you can omit deviceId: the API uses your default device, or otherwise the most recently active one.

FieldRequiredDescription
recipientsyesArray of phone numbers in international format, e.g. ["+14155550101"]
messageyesMessage text
deviceIdnoPick a specific phone
simSubscriptionIdnoPick the SIM on a dual-SIM phone
scheduledAtnoISO 8601 time in the future

cURL

bash
curl -X POST "https://smsportal.app/api/v1/gateway/send-sms" \
  -H "x-api-key: $SMSPORTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["+14155550101"],
    "message": "Your car is ready for pickup. Smith Auto, 12 Main St."
  }'

A successful call returns HTTP 200:

json
{
  "data": {
    "success": true,
    "message": "Message queued for delivery",
    "smsBatchId": "665f1c2a9b1e4a0012ab34cd"
  }
}

Keep the smsBatchId. You will use it to look up every message in that send.

Python

python
import os
import requests

API = "https://smsportal.app/api/v1"
HEADERS = {"x-api-key": os.environ["SMSPORTAL_API_KEY"]}

res = requests.post(
    f"{API}/gateway/send-sms",
    headers=HEADERS,
    json={
        "recipients": ["+14155550101"],
        "message": "Reminder: your appointment is tomorrow at 10:00. Reply YES to confirm.",
    },
    timeout=15,
)
res.raise_for_status()
print(res.json()["data"]["smsBatchId"])

Node.js

javascript
const API = 'https://smsportal.app/api/v1'

const res = await fetch(`${API}/gateway/send-sms`, {
  method: 'POST',
  headers: {
    'x-api-key': process.env.SMSPORTAL_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    recipients: ['+14155550101'],
    message: 'Order 1042 has shipped. Tracking: 5200123456.',
  }),
})

if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
console.log((await res.json()).data.smsBatchId)

PHP

php
<?php
$ch = curl_init('https://smsportal.app/api/v1/gateway/send-sms');

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 15,
    CURLOPT_HTTPHEADER => [
        'x-api-key: ' . getenv('SMSPORTAL_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'recipients' => ['+14155550101'],
        'message' => 'Hi! Your appointment is tomorrow at 10:00.',
    ]),
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("HTTP $status: $body");
}
echo json_decode($body, true)['data']['smsBatchId'];

Go

go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"recipients":["+14155550101"],"message":"Test from Go"}`)

	req, _ := http.NewRequest("POST",
		"https://smsportal.app/api/v1/gateway/send-sms",
		bytes.NewBuffer(body))
	req.Header.Set("x-api-key", os.Getenv("SMSPORTAL_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	out, _ := io.ReadAll(res.Body)
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, out))
	}
	fmt.Println(string(out))
}

For reusable helpers with retries and status polling, see send SMS from Python, Node.js and PHP. Field details are in the API docs.

Tip: there is no official SDK package on npm or pip. Plain fetch, requests or cURL is enough and adds no dependency.

How do you handle errors from the SMS API?

Check the HTTP status of every request. 200 means the job was accepted. 400, 401 and 429 each have a specific cause, and retrying them blindly will not help.

CodeCauseWhat to do
200Job acceptedStore smsBatchId
400No enabled device, empty message or recipients, invalid deviceId, unverified email, bad scheduledAtFix the input, the response body explains
401Missing, invalid or revoked API keyCheck the x-api-key header
429Daily, monthly or per-batch plan limit used upRead the body to see which limit, then wait or change plan

Python example:

python
res = requests.post(
    f"{API}/gateway/send-sms", headers=HEADERS, timeout=15,
    json={"recipients": ["+14155550101"], "message": "Test"},
)

if res.status_code == 200:
    print("OK", res.json()["data"]["smsBatchId"])
elif res.status_code == 429:
    print("Plan limit reached:", res.text)   # do not retry right away
elif res.status_code in (400, 401):
    print("Request error:", res.text)        # fix input or key
else:
    print("Server error, retry later:", res.status_code)

Remember that accepted does not mean delivered. The API returns 200 once the job is queued. The phone still has to be online and send the message.

How do you check whether an SMS was delivered?

Query GET /gateway/messages with the smsBatchId, or receive the status instantly through a webhook. A message moves from pending through dispatched and sent to delivered, or ends as failed.

bash
curl "https://smsportal.app/api/v1/gateway/messages?smsBatchId=665f1c2a9b1e4a0012ab34cd" \
  -H "x-api-key: $SMSPORTAL_API_KEY"

The response lists messages with status, recipient, sentAt and deliveredAt. To list only the failures from a batch, add &status=failed.

StatusMeaning
pendingAccepted, the phone has not processed it yet
dispatchedHanded to the phone
sentThe phone passed the SMS to the carrier network
deliveredThe carrier confirmed delivery (delivery report)
failedSending failed, e.g. invalid number or no signal

In the dashboard these appear as the labels queued (pending and dispatched), sent (sent), delivered (delivered) and failed (failed).

A missing delivery report is not always a failure: some networks and numbers never return one. Treat sent as "left the phone" and delivered as network confirmation.

Timeline of one message: ordered, queued, sent, delivered (Polish UI)

Instead of polling in a loop, set up a webhook for MESSAGE_SENT, MESSAGE_DELIVERED and MESSAGE_FAILED. Building and verifying one is covered in receive SMS replies with webhooks.

How do you receive replies through the API?

Replies to your number are stored as received messages. Fetch them with GET /gateway/messages?direction=received, or get a MESSAGE_RECEIVED webhook as they arrive.

bash
curl "https://smsportal.app/api/v1/gateway/messages?direction=received" \
  -H "x-api-key: $SMSPORTAL_API_KEY"

This is what makes the number a true two-way channel: a customer answers "YES" to an appointment reminder and your system confirms the booking. Webhook deliveries carry an X-Signature header (HMAC-SHA256 of the body) so you can verify them, and retried deliveries carry an idempotencyKey for deduplication. Full walkthrough: receive SMS with webhooks.

How do you send in bulk or schedule a text?

Use POST /gateway/send-bulk-sms for many messages with different text, and add scheduledAt to send-sms to send later.

Bulk, each entry has its own text and recipients:

bash
curl -X POST "https://smsportal.app/api/v1/gateway/send-bulk-sms" \
  -H "x-api-key: $SMSPORTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      { "recipients": ["+14155550101"], "message": "Hi Anna, your visit is tomorrow at 10:00." },
      { "recipients": ["+16475550187"], "message": "Hi Peter, your visit is tomorrow at 11:30." }
    ]
  }'

Scheduled (ISO 8601, in the future):

bash
curl -X POST "https://smsportal.app/api/v1/gateway/send-sms" \
  -H "x-api-key: $SMSPORTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["+14155550101"],
    "message": "Reminder: your appointment is tomorrow at 10:00.",
    "scheduledAt": "2026-10-08T08:00:00Z"
  }'

The server holds the message and dispatches it at that time. That is the backbone of automated appointment reminders. To send from a spreadsheet, the dashboard also accepts a CSV with column variables in the text, covered in bulk SMS from CSV.

On a dual-SIM phone, choose the SIM with simSubscriptionId. Without it, the phone's default SIM is used.

How many texts can you send, and what about character limits?

One phone sends about 12 texts per minute, because the app waits 5 seconds between messages by default. A GSM-7 segment holds 160 characters, and one non-GSM character switches the message to UCS-2 with 70 per segment.

  • Pace: 5 s between messages is roughly 720 per hour per phone. You can change the delay in the app. Large sends are released by the server in waves matched to that delay.
  • More volume: connect several phones (devices) and spread the sends.
  • Characters: "Your order has shipped" fits one segment. Add an emoji or an accented letter such as "ś" and the whole message becomes UCS-2, so the segment is 70 characters. Longer texts split into multiple segments.
  • Counter: the compose screen in the app shows characters and segments live.
Compose screen on the phone with segment counter (Polish UI)

When should you use a wholesale SMS API instead?

Use your own number for transactional, two-way and low-to-medium volume messages. Use a wholesale provider for large marketing blasts and when you need a branded sender ID.

SituationOwn number (smsportal)Wholesale SMS API
Appointment reminders, order updates, confirmationsVery goodGood
Customer replies to the textYes, to dashboard, API and webhookOften needs a separate inbound number
Cost per messageNo smsportal fee, you pay your mobile planPer-message fee, e.g. about $0.0083 per US segment plus carrier fees at Twilio, or about €0.035 per SMS to the US at SMSAPI.pl (as of October 2026, check current rates)
Alphanumeric sender IDNot availableAvailable in many countries
Tens of thousands of texts per campaignNoYes
US carrier registration (10DLC)A personal SIM is not registered, so carriers may filter automated trafficRequired for US application-to-person traffic, see Twilio's A2P 10DLC docs

Carriers can restrict bulk sending from consumer SIMs and it can breach their fair-use terms. So the honest advice is to send marketing blasts through a wholesale provider and keep one-to-one and transactional messages on your own number. For a deeper look, read when not to use an SMS gateway and 10DLC explained. Alternatives are compared in Twilio alternatives for small volume. Marketing texts also need recipient consent under local law. This is not legal advice.

How do you connect the SMS API to automations and AI agents?

The same API key works with no-code tools and with the MCP server for AI assistants. For automation, use an HTTP request node. For agents, use the ready-made MCP tools.

Everything that went through the API shows up on the phone too, as an activity list with filters and a per-message timeline.

Activity list in the app: sent and received SMS with filters (Polish UI)

How do you test the integration before going live?

Test in three steps: your own number, deliberate errors and an offline phone. You will see every status and error code before a customer does.

  1. Your own number. Send yourself a message and open its timeline in the app.
  2. Errors. Use a random key in x-api-key (expect 401), an empty message (expect 400) and a scheduledAt in the past (expect 400). Make sure your code does not retry them.
  3. Offline phone. Turn off the phone's internet, send a text and watch the pending status. When internet returns it should move to sent, then delivered.

In automated tests, do not send real texts. Replace the send function with a mock and assert it receives the right number and text. Add a separate test returning 401 and 500 to confirm that only transient errors are retried.

How do you protect the API key and customer data?

Keep the key in an environment variable or a secrets manager and treat phone numbers as personal data. You can revoke the key in the dashboard at any time, so generate a new one if you suspect a leak.

  • Separate keys for production, testing and each integration let you revoke one without breaking the others.
  • Never call the API from a browser. Make requests server-side, since anyone can read browser code.
  • Do not log message bodies containing one-time codes or health details. Log the smsBatchId and status instead.
  • Keep content minimal. SMS is not end-to-end encrypted to the recipient, so do not send passwords or full personal records.

Phone numbers fall under privacy law (GDPR in the EU), and marketing texts need consent. This is not legal advice.

What if a message never arrives?

Start with the status from the API, because it shows where the message got stuck. Most causes are on the phone side, not in your code.

Status or symptomLikely causeWhat to do
pending for a long timePhone offline or app asleepRestore internet, disable battery optimization
sent, never deliveredNetwork returns no delivery reportsUsually fine, the message may have arrived
failedBad number, no signal or no credit on the SIMCheck the international format and the SIM
HTTP 400 on sendNo enabled deviceEnable the phone in the dashboard
HTTP 429Plan limitCheck usage in the dashboard

If everything looks right, send a message by hand from the compose screen in the app. If that works but the API call does not, the problem is in the request. If manual sending fails too, look at the SIM and the carrier.

Production checklist before the first real customer

Check seven items before launch. It takes about 10 minutes and avoids the most common mistakes.

  1. The API key is in an environment variable, not in the repository.
  2. Your code checks the HTTP status and stores the smsBatchId.
  3. You retry only transient errors (timeouts, 5xx) with a growing delay.
  4. The phone has constant power, internet and battery optimization disabled.
  5. A webhook on MESSAGE_FAILED alerts you to failed sends.
  6. One-time codes have a short validity enforced by your own code, since the offline queue expires after 72 hours.
  7. You know the limit: about 720 texts per hour from one phone.

What are the most common problems?

Most problems are the phone, not your code. Start with the device health screen in the app and the API status code.

  • 400 "no enabled device": the phone is disabled in the dashboard or was never connected. Check the device list.
  • Status stuck on pending: the phone is offline or Android put the app to sleep. Disable battery optimization and check internet.
  • Status failed: invalid number, no credit or signal on the SIM, or a carrier block. Use the +1... or +48... international format.
  • No delivered status: the network did not return a delivery report. The message may still have arrived, and the status stays sent.
  • One-time codes: after 72 hours offline the queue expires, so give the code itself a short validity.
Device health: battery optimization, permissions and connectivity (Polish UI)

More on keeping the gateway stable is on the reliability page.

Send your first API text in 15 minutes

Create an account, connect a phone, generate a key and paste one of the examples above. The free plan covers testing the whole loop, including statuses and a webhook.

  1. Create a smsportal account.
  2. Install the Android app and scan the QR code.
  3. Run the curl example from your own computer to your own number, then reply to it.

When your volume grows, check the plans and the guide to Android SMS gateways.

Frequently asked questions

Is there any free API to send SMS?

No SMS API is truly free, because someone has to pay the carrier. With smsportal the text leaves your own SIM, so you pay your mobile plan (often unlimited texts) and there is no per-message fee from smsportal. There is also a free plan to start, so you can test the whole API at no cost.

How do I send an SMS using a REST API?

Send a POST request to /gateway/send-sms with an x-api-key header and a JSON body containing recipients (an array of E.164 numbers) and message. A 200 response means the request was accepted and returns a smsBatchId you use to check delivery status.

Can I receive replies through the API?

Yes. Replies arrive at your own number, so they show up in the dashboard, in GET /gateway/messages?direction=received, and as MESSAGE_RECEIVED webhook events. That makes two-way flows like replying YES to confirm an appointment possible.

Do I need 10DLC registration to send SMS this way?

10DLC registration applies to business application-to-person (A2P) traffic sent on 10DLC numbers through messaging providers. A personal SIM is not registered, so US carriers may filter or block automated traffic from it. In the US, an own-SIM gateway only fits low-volume, consented, conversational messaging. Read [10DLC explained](/en/blog/10dlc-do-you-need-it) before sending business traffic.

How many texts per minute can the API send?

The limit comes from the phone and the carrier, not the API. The app waits 5 seconds between messages by default, about 12 per minute or roughly 720 per hour per phone. For more volume, connect several phones or use a wholesale provider.

Is there an official SDK for the SMS API?

No. There is no official npm or pip package for smsportal. The API is plain HTTP with one header, so fetch, requests or cURL are all you need.

Send your first SMS today

Create a free account, connect your phone and see how customers respond to messages from your number.