Sending SMS
Send an SMS with POST /gateway/send-sms: request fields, response, status codes, phone number format, segments and error handling.
Updated
To send an SMS, call POST /gateway/send-sms with a message and a recipients array. textbee pushes the message to your Android phone, and the phone sends it through its SIM. The response returns an smsBatchId that you use to follow delivery.
/api/v1/gateway/send-sms, open in the API reference
Before you start
- A textbee account with a verified email.
- A registered device that is online.
- An API key, sent in the
x-api-keyheader.
Request fields
Send a JSON body. Only message and recipients are required.
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The text of the message. Long text is split into segments. |
recipients | string[] | Yes | Phone numbers in E.164 format. Each recipient counts as one message against your plan. |
deviceId | string | No | The phone that sends. Without it, textbee uses your default device, or else the enabled device with the most recent heartbeat. |
simSubscriptionId | number | No | The SIM that sends, on a dual-SIM phone. Without it, the phone uses its default SIM. The ID can change after a SIM swap, so copy the current one from the textbee app. See Send from a specific SIM. |
scheduledAt | string | No | An ISO 8601 time with a timezone, up to 72 hours ahead. Without it, the message goes now. See Schedule a message. |
Send a message
This sample sends one message from your default device. It reads the key from the TEXTBEE_API_KEY environment variable:
BASE_URL="${TEXTBEE_BASE_URL:-https://api.textbee.dev/api/v1}"
curl -sS -X POST "$BASE_URL/gateway/send-sms" \
-H "x-api-key: $TEXTBEE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipients": ["+12015550123"], "message": "Hello from textbee"}'Working in JavaScript or TypeScript? The @textbee/sdk package wraps this call with types and error classes.
Send from a specific device
This variant passes deviceId to choose the phone. Find the ID in the dashboard Devices panel or in GET /gateway/devices:
curl -X POST https://api.textbee.dev/api/v1/gateway/send-sms \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": ["+12015550123", "+12015550124"],
"message": "Your appointment is tomorrow at 10am",
"deviceId": "664a9b8cd0e1f2a3b4c5d6e7"
}'A malformed deviceId returns 400 with Invalid device id. An ID that is not on your account returns 404 with Device not found. textbee never falls back to another phone when you pass deviceId.
Response
A successful send returns 200:
{
"data": {
"success": true,
"message": "SMS added to queue for processing",
"smsBatchId": "66b1f2c3a4d5e6f7a8b9c0d2",
"recipientCount": 2
}
}| Field | Description |
|---|---|
success | true when textbee accepted the batch. |
message | A short outcome in words. |
smsBatchId | The batch ID. Use it to follow delivery. |
recipientCount | The number of recipients in the batch. |
estimatedCompletionAt | Only on large batches. The time the phone should finish, based on its Send Delay setting. The phone must stay online for the estimate to hold. |
Status codes
| Code | Meaning |
|---|---|
200 | textbee accepted the message. This is not delivery. |
400 | No enabled device to send from, the device is disabled, your email is not verified, or the message could not be pushed to the phone. The body says which. |
401 | The API key is missing, invalid or revoked. |
429 | Your daily, monthly or per-batch plan limit is used up. The body says which one. Nothing was sent. |
What happens after 200
The 200 response means textbee stored the message and queued it for your phone. The message has the status pending. textbee then pushes it to the phone. The phone hands it to the radio and reports sent when the carrier accepts it. If the carrier sends a delivery report, the status changes to delivered. If the phone is offline, the message waits until the phone comes back. Delivery status and message states explains each state and three ways to track it.
Phone number format
Write every number in E.164 format: a plus sign, the country code, then the number, with no spaces or punctuation.
| Correct | Incorrect |
|---|---|
+12015550123 | 2015550123 (no country code) |
+12015550124 | (201) 555-0124 (punctuation) |
+12015550125 | 001 201 555 0125 (international prefix instead of +) |
textbee removes spaces from each number. It does not add a missing country code. The phone passes the number to its carrier as written. The JavaScript SDK has isValidE164 and normalizePhoneNumber helpers to check numbers before you send.
Segments and encoding
The phone splits a long message into segments and sends each one. The recipient's phone joins them again. The segment size depends on the characters in the message:
| Encoding | When it applies | One message | Each segment of a longer message |
|---|---|---|---|
| GSM-7 | Only characters from the GSM-7 alphabet | 160 characters | 153 characters |
| UCS-2 | At least one other character | 70 characters | 67 characters |
One character outside GSM-7 switches the whole message to UCS-2. An emoji or a curly quote is enough. A 150-character message with one emoji needs three segments instead of one.
The JavaScript SDK has utilities for this: countSmsSegments, getSmsEncoding, findNonGsm7Characters and sanitizeForGsm7. They run locally and need no API key.
Handling errors
Check the status code of every response. Retry a 429 once after a pause, and do not retry 400 or 401 until you fix the cause. This sample shows the pattern:
BASE_URL="${TEXTBEE_BASE_URL:-https://api.textbee.dev/api/v1}"
# --retry repeats only on transient statuses such as 429; a 400 or 401 fails at once.
STATUS=$(curl -sS -o response.json -w '%{http_code}' --retry 1 --retry-delay 2 -X POST "$BASE_URL/gateway/send-sms" \
-H "x-api-key: $TEXTBEE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipients": ["+12015550123"], "message": "Hello from textbee"}')
case "$STATUS" in
200) jq -r '.data.smsBatchId' response.json ;;
401) echo "API key rejected: $(jq -r .message response.json)" >&2; exit 1 ;;
400) echo "Request rejected: $(jq -r .message response.json)" >&2; exit 1 ;;
*) echo "HTTP $STATUS: $(jq -r .message response.json)" >&2; exit 1 ;;
esacError bodies carry a human-readable reason. A 429 body has a message field that names the limit. Most 400 bodies have an error field, for example:
{ "success": false, "error": "No enabled device found. Enable a device or pass a deviceId." }Frequently asked questions
Does a 200 response mean the message was delivered?
No. It means textbee accepted the message. Follow the status with a webhook or the batch endpoint. See Delivery status and message states.
How many recipients can one request have?
Your plan's per-batch limit sets the maximum. A request over the limit returns 429 and sends nothing. For more numbers, split them into several requests. The pricing page lists the limits.
Can I send different text to each recipient?
Not with this endpoint. Use POST /gateway/send-bulk-sms, where each item has its own text and recipients.
Can I send MMS or images?
No. textbee sends text SMS only.
Why does the message go out from the wrong phone?
Without deviceId, textbee uses your default device. Set the default in the dashboard, or pass deviceId on each request.