Receiving SMS
Receive SMS sent to your Android phone through textbee: turn on forwarding, then get each message by webhook or by polling the messages endpoint.
Updated
textbee forwards each SMS that arrives on your registered Android phone to your account. Your server gets the message in one of two ways: a webhook that textbee pushes to your URL, or a poll of the messages endpoint. Turn on Receive SMS in the app first, because nothing is forwarded without it.
How receiving works
- A person sends an SMS to the phone number of the SIM in your phone.
- The textbee app on the phone reads the message and uploads it to textbee.
- textbee stores the message in your history with
direction: "received"andstatus: "received". - textbee sends a
MESSAGE_RECEIVEDevent to each webhook subscription that asks for it. - Your code reads the message from the webhook body or from
GET /gateway/messages.
If the phone is offline when the SMS arrives, the app uploads it when the phone reconnects. The message then appears in history and webhooks at that later time. See Late uploads for very late messages.
Turn on receiving
- Open the textbee app on the phone.
- Go to Settings. In the SMS section, turn on Receive SMS ("Forward incoming SMS to backend").
- Grant the SMS permission when Android asks for it. The app cannot read incoming SMS without it.
- Send a test SMS to the phone from another phone.
- Check the dashboard. The message appears in the message history.
The device record shows the setting too. GET /gateway/devices returns receiveSMSEnabled for each phone. It must be true for received history and webhooks. See Managing devices for the other device fields.
If a test SMS does not appear, open Settings > Device health in the app. It lists the SMS permission, the Receive SMS toggle and battery settings that stop the app in the background.
Webhook or polling
Both methods return the same messages. Pick the one that fits your server.
| Webhook | Polling | |
|---|---|---|
| Latency | Seconds after the phone uploads the message | Your poll interval |
| Needs a public URL | Yes, HTTPS in production | No |
| Retries | textbee retries failed deliveries, up to 10 attempts | Not needed. The next poll reads what you missed. |
| Setup | A subscription and an endpoint that verifies the signature | A loop or a scheduled job |
| Pick it when | You reply fast, for example to a code or a keyword | Your server is behind a firewall, or you run a batch job |
Many integrations use both. The webhook handles new messages, and a daily poll fills any gap.
Receive with a webhook
A webhook is a URL on your server. textbee sends an HTTP POST to it for each event you select.
- In the dashboard, open Webhooks and add a webhook.
- Enter your URL and a signing secret of at least 20 characters.
- Select the MESSAGE_RECEIVED event.
- Return a 2xx status quickly from your endpoint.
The full setup, the signature check and the retry rules are on the Webhooks page. Every event payload is on the Webhook events reference.
Receive by polling
The messages endpoint returns sent and received messages for every device on your account.
GET/api/v1/gateway/messages, open in the API reference
This request returns the 20 newest received messages:
curl "https://api.textbee.dev/api/v1/gateway/messages?direction=received&limit=20" \
-H "x-api-key: YOUR_API_KEY"To read only new messages on each run, ask for order=asc and follow the cursor. The next poll resumes from the last nextCursor you stored. This sample reads every received message since a start time:
BASE_URL="${TEXTBEE_BASE_URL:-https://api.textbee.dev/api/v1}"
QUERY="direction=received&order=asc&limit=50&from=2026-09-01T00:00:00Z"
CURSOR=""
while :; do
PAGE=$(curl -sS "$BASE_URL/gateway/messages?$QUERY${CURSOR:+&cursor=$CURSOR}" -H "x-api-key: $TEXTBEE_API_KEY")
echo "$PAGE" | jq -r '.data[] | "\(._id) \(.sender) \(.message)"'
[ "$(echo "$PAGE" | jq -r '.meta.hasMore')" = "true" ] || break
CURSOR=$(echo "$PAGE" | jq -r '.meta.nextCursor') # store this to resume the next poll
doneThe cursor never skips a message and never returns one twice. Message history and polling has the full parameter table and more filters.
Response shape
Each received message in data looks like this excerpt:
{
"data": [
{
"_id": "66f7c1e2a4d5e6f7a8b9c0d1",
"message": "YES, please confirm my booking",
"sender": "+12015550123",
"receivedAt": "2026-09-28T09:14:03.000Z",
"direction": "received",
"status": "received",
"device": {
"_id": "664a9b8cd0e1f2a3b4c5d6e7",
"enabled": true,
"name": "Office phone",
"brand": "google",
"model": "Pixel 7",
"buildId": "BP2A.250605.031"
},
"createdAt": "2026-09-28T09:14:05.112Z",
"updatedAt": "2026-09-28T09:14:05.112Z"
}
],
"meta": {
"limit": 20,
"nextCursor": "eyJ0IjoiMjAyNi0wOS0yOFQwOToxNDowNS4xMTJaIn0",
"hasMore": true
}
}| Field | Meaning |
|---|---|
sender | Number that sent the SMS, as the phone reports it |
message | Message text |
receivedAt | When the phone received the SMS |
direction | received for incoming messages |
device._id | Device that received the message. Use it as deviceId. |
Reply to a received message
To reply, send an SMS to the sender number with the send endpoint. Pass the device._id of the received message as deviceId so the reply goes out from the same phone.
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"], "message": "Thanks, your booking is confirmed.", "deviceId": "664a9b8cd0e1f2a3b4c5d6e7"}'On a phone with two SIMs, pass simSubscriptionId as well. See Choosing a SIM.
Notes
Messages received over the plan limit
A received message that arrives while your account is over its plan limit has overLimit: true. The field is absent on other messages. See Pricing for the limits of each plan.
Late uploads
A phone that was offline uploads its backlog when it reconnects. If the upload is more than 24 hours after receivedAt, textbee sets createdAt to receivedAt and stores the upload time in originalCreatedAt. The from and to filters work on createdAt, so the message lands in the time window when it was received.
If the upload is more than 48 hours after receivedAt, textbee stores the message but does not send a MESSAGE_RECEIVED webhook for it. A poll still returns it. This is one reason to run a periodic poll next to a webhook.
Filter messages on the phone
The app can forward only some messages, for example only messages from one short code. Filtered messages never leave the phone. See Filter incoming SMS on the phone.
Frequently asked questions
Why do I not see received messages?
Check four things. Receive SMS is on in the app. The SMS permission is granted. receiveSMSEnabled is true on the device. No SMS filter blocks the sender. The phone connection guide covers phones that stop in the background.
Can one account receive on several phones?
Yes. Each registered phone forwards its own messages. The messages endpoint returns all of them. Use deviceIds to narrow the list to one phone.
Does textbee receive MMS?
No. textbee receives SMS text messages only.
Do I need a webhook to receive messages?
No. The messages endpoint returns received messages without a webhook. A webhook only removes the delay of a poll.
Next steps
- Message history and polling: every filter and both pagination modes
- Webhooks: push delivery, signatures and retries
- Webhook events reference: the
MESSAGE_RECEIVEDpayload and the other events - Filter incoming SMS on the phone: forward only the messages you need
- Sending SMS: reply to a received message