Send SMS via API: Python, Node.js, PHP and cURL Examples
Send SMS with an API in Python, Node.js, PHP and cURL: single and bulk sends, Unicode and Arabic, segment counting, delivery status webhooks and error handling.
By OmniChannels.ai Editorial Team · Reviewed by BroadNet Messaging Solutions Team
Integration
Short answer
To send an SMS with the OmniChannels.ai API, make an authenticated POST to https://api.broadnet.me/sms/sendSms with a JSON body containing senderId, destinationNumber, message and messageType. Use TEXT for GSM-7 text and UNICODE for Arabic or emoji. A 200 response returns the message ID as plain text, and the final delivery status arrives later at your webhook.
On this page
- How do I send an SMS with the API?
- How should I format phone numbers?
- How do I send Arabic, emoji or other Unicode text?
- How do I count segments before sending?
- How do I send to many numbers?
- How do I receive delivery status?
- How do I handle errors and avoid duplicates?
- Worked example: login codes for a regional app
- What are the common mistakes?
- How OmniChannels.ai helps
Key takeaways
- POST /sms/sendSms needs four fields: senderId, destinationNumber, message and messageType; the response body is the message ID as plain text, not JSON.
- Set messageType to UNICODE for Arabic, emoji or any character outside GSM-7; one such character makes the whole message Unicode and shortens each segment.
- POST /sms/identify counts characters and segments before you send, so you can predict how many segments a message uses.
- Delivery is asynchronous: a 200 means accepted, and DELIVERED, FAILED or REJECTED arrives later at your webhook.
- The API has no idempotency key, so record each send before you make it and check status after a timeout instead of resending.
This guide is for developers who need to send SMS from an application and want working code in Python, Node.js, PHP or cURL. It covers a single send, Unicode and Arabic, counting segments, bulk upload, delivery status webhooks and error handling, using only endpoints and fields from our SMS API reference and OpenAPI spec.
How do I send an SMS with the API?
Make an authenticated POST to /sms/sendSms with a JSON body. The response is 200 OK with the message ID as plain text.
| Item | Value |
|---|---|
| Base URL | https://api.broadnet.me (no /v1 prefix) |
| Endpoint | POST /sms/sendSms |
| Authentication | Authorization: Bearer $BROADNET_TOKEN |
| Content type | application/json |
| Required fields | senderId, destinationNumber, message, messageType |
Test credentials are issued after a short access request, and test sends go to your own verified numbers. There is no instant self-serve signup. See the quickstart.
curl -X POST https://api.broadnet.me/sms/sendSms \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"senderId": "OmniDemo",
"destinationNumber": "447700900123",
"message": "Your OmniDemo code is 482913",
"messageType": "TEXT"
}'
// Node.js 18+ (built-in fetch)
const res = await fetch("https://api.broadnet.me/sms/sendSms", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BROADNET_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
senderId: "OmniDemo",
destinationNumber: "447700900123",
message: "Your OmniDemo code is 482913",
messageType: "TEXT",
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const messageId = await res.text(); // plain-text message ID
import os
import requests
r = requests.post(
"https://api.broadnet.me/sms/sendSms",
headers={"Authorization": f"Bearer {os.environ['BROADNET_TOKEN']}"},
json={
"senderId": "OmniDemo",
"destinationNumber": "447700900123",
"message": "Your OmniDemo code is 482913",
"messageType": "TEXT",
},
timeout=15,
)
r.raise_for_status()
message_id = r.text # plain-text message ID, not JSON
<?php
$payload = json_encode([
"senderId" => "OmniDemo",
"destinationNumber" => "447700900123",
"message" => "Your OmniDemo code is 482913",
"messageType" => "TEXT",
]);
$ch = curl_init("https://api.broadnet.me/sms/sendSms");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("BROADNET_TOKEN"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $payload,
]);
$messageId = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
echo $status . " " . $messageId . PHP_EOL;
A successful response body looks like this:
2603121407537573731
Save that ID with your own record, such as an order or login attempt. It is how you match delivery statuses later.
How should I format phone numbers?
Use country code plus national number, digits only. The format follows the international numbering plan in ITU-T E.164, minus the plus sign.
| Input | Valid? | Why |
|---|---|---|
447700900123 |
Yes | Country code 44 plus national number |
+44 7700 900123 |
No | Remove the plus sign and spaces |
07700900123 |
No | Replace the leading 0 with the country code |
Store numbers in E.164 (+447700900123) in your database and strip the + when you build the request. Our free phone number formatter helps with a quick check.
How do I send Arabic, emoji or other Unicode text?
Set messageType to UNICODE. Any character outside the GSM-7 alphabet, such as an Arabic letter, emoji, curly quote or em dash, switches the whole message to Unicode.
r = requests.post(
"https://api.broadnet.me/sms/sendSms",
headers={"Authorization": f"Bearer {os.environ['BROADNET_TOKEN']}"},
json={
"senderId": "OmniDemo",
"destinationNumber": "971500000000",
"message": "رمز التحقق الخاص بك هو 482913",
"messageType": "UNICODE",
"campaignName": "login-otp",
},
timeout=15,
)
In PHP, use JSON_UNESCAPED_UNICODE when you call json_encode. Encoding decides segment size, as defined for SMS in 3GPP TS 23.040:
messageType |
Single SMS | Per segment when split | Use for |
|---|---|---|---|
TEXT |
160 characters | 153 characters | Text using only the GSM-7 alphabet |
UNICODE |
70 characters | 67 characters | Arabic, Cyrillic, Chinese, emoji |
Most operators bill per segment, so a long Arabic message costs more than the same length in Latin script. Read our Unicode and Arabic SMS guide and try the SMS character counter.
How do I count segments before sending?
Call POST /sms/identify with the message. It returns the detected encoding, character count, segment count and per-segment limit.
curl -X POST https://api.broadnet.me/sms/identify \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "message": "Your OmniDemo code is 482913" }'
{
"message": "Your OmniDemo code is 482913",
"messageType": "TEXT",
"smscount": 1,
"charCount": 28,
"charLimit": 160
}
You can pass smsParts (from smscount), charCount and charLimit into a send request. GSM-7 text may be labelled GSM-7 in the response; send it as TEXT.
How do I send to many numbers?
Use POST /sms/sendbulk, a multipart/form-data request with a recipient file and a JSON string of settings.
curl -X POST https://api.broadnet.me/sms/sendbulk \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-F destinationNumberFile=@recipients.csv \
-F 'bulkRequest={"senderId":"OmniDemo","message":"Your statement is ready in the app.","messageType":"TEXT","campaignName":"oct-statements","allowDuplicate":false}'
import json
import os
import requests
bulk_request = {
"senderId": "OmniDemo",
"message": "Your statement is ready in the app.",
"messageType": "TEXT",
"campaignName": "oct-statements",
"allowDuplicate": False,
}
with open("recipients.csv", "rb") as f:
r = requests.post(
"https://api.broadnet.me/sms/sendbulk",
headers={"Authorization": f"Bearer {os.environ['BROADNET_TOKEN']}"},
files={"destinationNumberFile": ("recipients.csv", f, "text/csv")},
data={"bulkRequest": json.dumps(bulk_request)},
timeout=60,
)
print(r.json())
The response includes bulkSessionId and a bulkListInfo object with total, validCount, invalidCount and duplicate. Store bulkSessionId with the campaign. Jobs can be listed, paused, resumed and aborted with the endpoints in the reference. You can also check numbers before a big send with POST /sms/lookup; see our HLR lookup guide.
How do I receive delivery status?
Delivery statuses are pushed to the webhook URL configured on your account. A message moves through SUBMITTED and SENT to DELIVERED, or ends as FAILED or REJECTED. You give our team the public HTTPS URL, and resellers can have one webhook per sub-account.
Payload fields vary by channel, and the API spec does not define a single webhook schema, so log the raw body first and map fields from a real event on your account. A minimal receiver stores the event and acknowledges quickly:
import express from "express";
const app = express();
app.post("/webhooks/omnichannels", express.json(), async (req, res) => {
await saveStatusEvent(req.body); // your storage; process asynchronously
res.sendStatus(200); // acknowledge fast
});
app.listen(3000);
Make processing idempotent, because the same status can arrive twice, and never let a late SENT overwrite a stored DELIVERED. If your endpoint was down, POST /sms/resendDlr re-pushes receipts for a date range:
curl -X POST https://api.broadnet.me/sms/resendDlr \
-H "Authorization: Bearer $BROADNET_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startDate": "2026-10-09",
"endDate": "2026-10-10",
"senderId": "OmniDemo",
"messageStatus": "DELIVERED"
}'
Read more in the webhooks guide and in message delivery statuses explained.
How do I handle errors and avoid duplicates?
Handle the HTTP status first, then the error body. Retry only network errors, 429 and 5xx.
| Code | Meaning | Action |
|---|---|---|
200 |
Accepted | Store the ID |
400 |
Validation error or missing field | Fix the request |
401 / 403 |
Bad token or operation not allowed | Fix; do not retry |
429 |
Rate limit exceeded | Wait for Retry-After |
500, 503, 504 |
Server or upstream trouble | Retry with backoff and jitter |
Errors raised by the API use a five-field JSON body with status, error, message, errorCode and requestUri. The default documented REST limit is 200 requests per second sustained; your account's limits are confirmed with your credentials.
The API accepts no idempotency key. A request can succeed while its response is lost, and a blind retry then sends twice. Write a pending row keyed by your own operation ID before calling, store the returned message ID, and after a timeout check for a webhook status before sending again. For bulk sends, set allowDuplicate to false. Details are in Errors and limits.
Worked example: login codes for a regional app
"Souq Rafiq" (synthetic), a UAE retailer, sends login codes by SMS.
- The auth service generates a six-digit code, stores a hash with a five-minute expiry and writes a
pendingsend row. - It calls
/sms/sendSmswithmessageType=UNICODEfor Arabic users andTEXTfor English users. - On
200, it stores the message ID. - The webhook handler marks the row
DELIVERED,FAILEDorREJECTED. - If the status is not
DELIVEREDafter a short wait, the app offers a resend and, if configured, another channel. See channel fallback. - Rate limits on code requests per number and per IP protect against abuse; see SMS pumping prevention.
What are the common mistakes?
- Calling
r.json()on the send response. It is plain text. - Wrong field names. Use
senderId,destinationNumberandmessage; fields such asto,fromortextare not part of this endpoint. - Plus signs and leading zeros in numbers.
- Forgetting
UNICODEfor Arabic, which garbles the text. - Unregistered sender IDs. Some countries block or replace them; check the sender ID rules for your destination, and ask us to handle the registration.
- Treating
200as delivered. - Token in a mobile app or browser, where anyone can extract it.
- Limits: this guide shows the common request. Optional fields such as scheduling formats should be confirmed with our team.
How OmniChannels.ai helps
OmniChannels.ai sends A2P SMS over BroadNet's direct operator connectivity, with two-way SMS, sender IDs, Unicode and Arabic, HLR lookup and SMPP 3.4 for high volume. The REST API at https://api.broadnet.me uses a Bearer token and also covers WhatsApp, RCS and other channels, with delivery statuses normalised into one webhook format. Our team prepares sender ID registrations with you. See the SMS API reference or the SMS channel. The same API calls are part of a CPaaS platform and messaging API.
Frequently asked questions
How do I send an SMS with Python?
Use the requests library to POST JSON to https://api.broadnet.me/sms/sendSms with an Authorization: Bearer header and the fields senderId, destinationNumber, message and messageType. Read the token from an environment variable. A 200 response body is the message ID as plain text, so use r.text rather than r.json().
What phone number format does the SMS API expect?
Country code plus national number, digits only, with no plus sign, 00 prefix, spaces or leading zero. For example, 447700900123 is valid, while +44 7700 900123 and 07700900123 are not. Store numbers in E.164 in your own database and strip the plus sign when building the request.
How do I send Arabic SMS through the API?
Set messageType to UNICODE and send the Arabic text in the message field as UTF-8. Unicode messages hold 70 characters in a single segment and 67 per segment when split, compared with 160 and 153 for GSM-7. Test on real handsets, and call /sms/identify to see the segment count first.
How do I get delivery reports?
Statuses are pushed to the webhook URL configured on your account. Messages move through SUBMITTED and SENT to DELIVERED, FAILED or REJECTED. Your endpoint should store the raw event, answer 2xx quickly and process asynchronously. If events were missed, POST /sms/resendDlr can re-push delivery receipts for a date range.
How do I send SMS to many numbers at once?
Use POST /sms/sendbulk, a multipart request with a destinationNumberFile (CSV or TXT) and a bulkRequest JSON string holding senderId, message and messageType. The response reports how many numbers were valid, invalid or duplicate, and returns a bulkSessionId you can store with your campaign.
Do I need to register a sender ID?
In many countries, yes. Some require an approved alphanumeric sender ID, and others need regulator or template identifiers, which the API accepts in optional fields such as peId and templateId. Requirements are country-specific, so check the sender ID page for your destination, and our team can handle the registration with you.
Sources
Keep reading
More guidesIntegration
Send a WhatsApp Message via API: cURL, Node, Python, PHP
How to send a WhatsApp message with an API: working cURL, Node.js, Python and PHP examples for templates and session messages, plus status tracking and errors.
10 min read
Integration
How to Choose a Messaging API: A Checklist for CTOs
How to choose a messaging API: a CTO checklist for channels, route quality, webhooks, security, rate limits, retries, SMPP, MCP, compliance and pricing.
13 min read
SMS & OTP
SMS Link Tracking: How to Use Short Links in SMS
SMS link tracking explained: short links, UTM parameters, branded vs generic shorteners, character budget, click and conversion measurement and privacy rules.
10 min read
