textbee Logotextbee.dev
Save 44% with yearly billing.View Plans
Documentation

Message history and polling

List sent and received SMS across every device with GET /gateway/messages: filters, page and cursor pagination, and polling without gaps.

Updated

The messages endpoint returns sent and received messages for every device on your account, newest first. Filter it by direction, status, device, batch, text and time. Use page numbers to browse, and use the cursor to poll for new messages without gaps or duplicates.

The endpoint

GET/api/v1/gateway/messages, open in the API reference

This request returns the 50 newest messages in both directions:

Shell
curl "https://api.textbee.dev/api/v1/gateway/messages" \
  -H "x-api-key: YOUR_API_KEY"

Parameters

All parameters are optional query parameters. An unknown filter value fails with a 400 error. textbee does not ignore it.

ParameterValuesDescription
directionall, sent, receivedWhich messages to return. Default all.
statuspending, dispatched, sent, delivered, failed, unknown, receivedDelivery state. direction=sent&status=failed lists sends that failed.
deviceIdsComma-separated device IDsOnly these devices. Default: every device on the account.
smsBatchIdA batch IDOnly messages from this batch, using the smsBatchId a send returns.
searchTextMatches the message text and the other party's number.
fromISO 8601 datetime with a timezone, or a dateInclusive lower bound on createdAt. A date such as 2026-09-01 is read as UTC midnight.
toSame formats as fromExclusive upper bound on createdAt.
orderdesc, ascdesc (default) is newest first. Use asc to walk forward in time when you poll.
pageInteger, 1 or morePage to return. Default 1. Do not combine with cursor.
limit1 to 100Messages per page. Default 50.
cursorOpaque stringPosition from a previous meta.nextCursor. Returns the page after that position.

A datetime without a timezone, such as 2026-09-01T00:00:00, is rejected. Use 2026-09-01T00:00:00Z or 2026-09-01T00:00:00+03:00.

from is inclusive and to is exclusive. Back-to-back windows, such as one day and the next, never count a message twice.

direction and status are different things. direction=sent means outgoing messages. status=sent means the phone reported the message as sent.

A deviceIds entry or an smsBatchId that is not on your account returns 404.

Page and cursor pagination

The endpoint has two pagination modes. The mode depends on whether you pass cursor.

Page mode

Without cursor, the response has page numbers and a total count:

JSON
{
  "data": [],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 1284,
    "totalPages": 26,
    "nextCursor": "eyJ0IjoiMjAyNi0wOS0yOFQwOToxNDowNS4xMTJaIn0",
    "hasMore": true
  }
}

Use page mode to show a table with page numbers in your own UI.

Cursor mode

With cursor, the response has no total count. It is faster on large histories:

JSON
{
  "data": [],
  "meta": {
    "limit": 50,
    "nextCursor": "eyJ0IjoiMjAyNi0wOS0yOFQxMDowMjoxNy4wMDBaIn0",
    "hasMore": false
  }
}

nextCursor is null on the last page. Treat the cursor as an opaque string. Do not build or change it yourself.

Use cursor mode to read a full history or to poll for new messages. Page numbers drift when new messages arrive during the read. A cursor does not drift.

Poll without gaps or duplicates

To poll for new received messages:

  1. On the first run, request direction=received&order=asc with a from time.
  2. Process each message in data.
  3. While meta.hasMore is true, request the next page with cursor=<meta.nextCursor>.
  4. Store the last nextCursor you received, for example in your database.
  5. On the next run, start with that stored cursor instead of from.

This sample runs one poll from a start time and follows the cursor:

poll.sh
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
done

The stored cursor is your resume point. The next run starts after the last message you processed, so it never re-reads a page and never returns a message twice.

Send the same filters with every request of one poll. Change only the cursor.

Most late uploads are safe. A phone that was offline uploads its backlog when it reconnects, and the next poll returns those messages. A message uploaded more than 24 hours late gets createdAt equal to receivedAt, so a poll that already moved past that time does not return it. For an exact record, run a daily poll with from and to over the previous two days and deduplicate on _id.

Useful filters

Failed sends of one batch. Use the smsBatchId from the send response:

Shell
curl "https://api.textbee.dev/api/v1/gateway/messages?smsBatchId=66f7c3a8b1c2d3e4f5a6b7c0&status=failed" \
  -H "x-api-key: YOUR_API_KEY"

Received messages on one device in one week:

Shell
curl "https://api.textbee.dev/api/v1/gateway/messages?direction=received&deviceIds=664a9b8cd0e1f2a3b4c5d6e7&from=2026-09-21&to=2026-09-28" \
  -H "x-api-key: YOUR_API_KEY"

All messages to or from one number. Encode the + as %2B:

Shell
curl "https://api.textbee.dev/api/v1/gateway/messages?search=%2B12015550123" \
  -H "x-api-key: YOUR_API_KEY"

Several devices at once:

Shell
curl "https://api.textbee.dev/api/v1/gateway/messages?deviceIds=664a9b8cd0e1f2a3b4c5d6e7,664a9b8cd0e1f2a3b4c5d6f1" \
  -H "x-api-key: YOUR_API_KEY"

The JavaScript SDK wraps these filters in getMessages and follows the cursor for you with iterateMessages.

Response fields worth knowing

FieldDescription
_idMessage ID. It matches smsId in webhook payloads.
directionsent or received, lowercase. Pass it back as the direction filter.
statusDelivery state. Received messages are always received.
senderSender number. Received messages only.
recipientDestination number. Sent messages only.
messageMessage text. It can be an empty string.
deviceObject with the device _id, enabled, name, brand and model.
smsBatchBatch ID. Sent messages only.
simSubscriptionIdSIM the message went out from, when set.
receivedAtWhen the phone received the message.
requestedAt, dispatchedAt, sentAt, deliveredAt, failedAtTimestamps of an outgoing message. See Delivery status and message states.
errorCode, errorMessageFailure details of a failed message.
overLimittrue on a received message that arrived while the account was over its plan limit. Absent otherwise.
createdAtWhen textbee stored the message. The from and to filters use this field.
originalCreatedAtUpload time. Set only on a received message uploaded more than 24 hours after receivedAt.

A missing status on an old message means the message predates status tracking. Treat it as unknown.

Messages from deleted devices are never included. If you delete a device in the dashboard, its messages leave the history. See Managing devices.

Frequently asked questions

Do I need a device ID to read messages?

No. The endpoint covers every device on your account. Use deviceIds only to narrow the list.

Why does a poll miss a message I see in the dashboard?

Check the filters. A from time after the message's createdAt, a deviceIds list without its device, or a different direction hides it. Also check the stored cursor: it resumes after the last message of the previous run.

Can I get messages of one conversation?

Yes. Use search with the other party's number. It matches both sent and received messages.

Is polling or a webhook better?

A webhook is faster. A poll needs no public URL and catches anything a webhook missed. See the comparison on Receiving SMS.

Next steps