Android Texter turns an Android phone into an SMS gateway. Messages you send through this API are delivered by the SIM in a phone you have linked to your account, from your own number, and replies are synced back and can be pushed to a webhook. There are no per-message carrier fees and no shared short codes.
curl -X POST "https://androidtexter.com/api/messages/send" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+14155551234",
"message": "Hello from the API!"
}'Optional. Pasting a key rewrites every sample on this page to use it and enables the live Try it buttons on read-only endpoints. It is kept in this tab only, is never sent anywhere except https://androidtexter.com, and is forgotten when you close the tab.
Every request is authenticated with an API key in the X-API-Key header. Dashboard-only endpoints instead take a JWT in Authorization: Bearer <token>.
X-API-Key: atx_your_key_hereIdentify the account a request belongs to. Send an API key in the X-API-Key header, or a JWT bearer token for dashboard session endpoints.
/api/meReturns the authenticated user's profile and subscription info. Useful as a credential check.
curl -X GET "https://androidtexter.com/api/me" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"user": { "id": "...", "name": "Phil", "email": "[email protected]" },
"subscription": { "plan": "free", "status": "active" }
}Send and read SMS through your connected Android phones.
/api/messages/sendSend an SMS to a phone number. Optionally choose which linked device sends it, and a future time to deliver it. A number without a country code is read in the country of the SIM sending it; pass country (ISO 3166-1 alpha-2) to read it in another country instead.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| phone | body | yes | +14155551234 |
| message | body | yes | Hello from the API! |
| device_id | body | no | device UUID |
| scheduled_at | body | no | 2026-03-12T09:00:00Z |
| country | body | no | GB |
curl -X POST "https://androidtexter.com/api/messages/send" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+14155551234",
"message": "Hello from the API!"
}'Example response
{
"status": 200,
"message": "Message queued for sending on Pixel 7",
"message_id": "uuid",
"device_id": "uuid",
"device_name": "Pixel 7"
}/api/messages/scheduledList pending scheduled messages that have not been sent yet.
curl -X GET "https://androidtexter.com/api/messages/scheduled" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"scheduled_messages": [
{
"id": "uuid",
"phone": "+14155551234",
"body": "Reminder: appointment tomorrow!",
"scheduled_at": "2026-03-12T09:00:00Z",
"scheduler_status": "pending",
"device_id": "uuid"
}
]
}/api/messages/scheduled/:idCancel a pending scheduled message before it is sent.
curl -X DELETE "https://androidtexter.com/api/messages/scheduled/:id" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "message": "Scheduled message cancelled" }/api/messagesList all messages, inbound and outbound, newest first.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| page | query | no | 1 |
| limit | query | no | 50 |
curl -X GET "https://androidtexter.com/api/messages?page=1&limit=50" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"messages": [
{ "id": "...", "phone": "+1...", "body": "Hello", "direction": "inbound", "created_at": "..." }
]
}/api/messages/sentList outbound messages only.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| page | query | no | 1 |
| limit | query | no | 50 |
curl -X GET "https://androidtexter.com/api/messages/sent?page=1&limit=50" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "messages": [] }/api/messages/receivedList inbound messages only.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| page | query | no | 1 |
| limit | query | no | 50 |
curl -X GET "https://androidtexter.com/api/messages/received?page=1&limit=50" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "messages": [] }/api/v1/messages/history/:phoneFetch paginated historical messages scoped to a single contact phone number. For a group conversation also pass thread_id (or thread_ref) from an inbound message, and the group's messages are returned instead.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| page | query | no | 1 |
| limit | query | no | 50 |
| thread_id | query | no | |
| thread_ref | query | no |
curl -X GET "https://androidtexter.com/api/v1/messages/history/:phone?page=1&limit=50&thread_id=&thread_ref=" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"messages": [
{ "id": "...", "phone": "+1...", "body": "Hello", "direction": "inbound", "created_at": "..." }
],
"phone": "+1...",
"page": 1,
"limit": 50
}/api/v1/messages/status/:idLook up the delivery state of one message by its id, as returned by Send Message.
curl -X GET "https://androidtexter.com/api/v1/messages/status/:id" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "id": "uuid", "status": "sent", "sent_at": "..." }/api/messages/conversationsA grouped list of conversations with the latest message from each phone number. Pass view to pick a folder: inbox (someone has written back), outbox (nothing has come back yet), archived, spam, or all (everything except archived and spam). The older filter parameter still works and is unchanged: filter=inbox is everything except spam, archived included, filter=spam is only spam, filter=all is everything including spam.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| limit | query | no | 50 |
| view | query | no | all |
curl -X GET "https://androidtexter.com/api/messages/conversations?limit=50&view=all" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"conversations": [
{
"thread_ref": "uuid",
"phone": "+1...",
"last_message": "Hey!",
"last_direction": "inbound",
"last_at": "...",
"message_count": 12,
"has_inbound": true,
"archived": false
}
]
}/api/messages/conversations/countsHow many conversations are in each folder. A conversation whose only message is a scheduled send that has not gone out yet is in none of them, the same as in the list itself.
curl -X GET "https://androidtexter.com/api/messages/conversations/counts" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"counts": { "all": 412, "inbox": 96, "outbox": 310, "archived": 6, "spam": 3 }
}/api/messages/conversations/archivePut a conversation in the Archived folder, or take it out again. Archiving only changes which folder it is in: notifications still arrive, linked phones are not told, and the conversation is still returned by view=all. An incoming message un-archives it automatically. Name the conversation with thread_ref (from List Conversations), with thread_id, or with phone.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| thread_ref | body | yes | uuid, from List Conversations |
| phone | body | yes | (instead of thread_ref) +14155551234 |
| archived | body | yes | true to archive, false to unarchive |
curl -X POST "https://androidtexter.com/api/messages/conversations/archive" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"thread_ref": "uuid, from List Conversations",
"phone": "(instead of thread_ref) +14155551234",
"archived": "true to archive, false to unarchive"
}'Example response
{
"success": true,
"thread_ref": "uuid",
"archived": true
}/api/messages/conversations/archivedEvery archived conversation, as identities only: the member phone numbers (comma-joined for a group), the handset thread id when one is known, and when it was archived. This is what the Android app reconciles its own archive against, and it is the endpoint to poll if you keep your own copy: unlike view=archived it is not capped at a limit and it does include conversations that are also flagged as spam.
curl -X GET "https://androidtexter.com/api/messages/conversations/archived" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"threads": [
{
"phone": "+14155551234",
"thread_id": "42",
"archived_at": "2026-11-01T18:04:11Z"
}
]
}The Android phones linked to your account. A phone must be online to send.
/api/devicesReturns every device linked to your account with its online status. Use a device id to pin a send to one phone.
curl -X GET "https://androidtexter.com/api/devices" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"devices": [
{
"id": "uuid",
"name": "Pixel 7",
"phone_number": "+14155551234",
"online": true,
"last_seen": "2026-03-11T20:00:00Z"
}
]
}/api/messages/threadPermanently delete a conversation, every message in it, and the pictures in it that no other conversation uses, for the whole account. Linked phones are told to delete their copy too, by push and on their next heartbeat. Name the conversation with thread_ref (from List Conversations), or with phone. Not reversible; deleting the same conversation twice returns 404.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| thread_ref | query | no | uuid |
| phone | query | no | +14155551234 |
curl -X DELETE "https://androidtexter.com/api/messages/thread?thread_ref=uuid&phone=+14155551234" \
-H "X-API-Key: YOUR_API_KEY"Example response
{
"success": true,
"thread_ref": "uuid",
"phone": "+14155551234",
"messages_deleted": 42,
"devices_notified": 1,
"devices_pending_update": 0
}/api/devices/:idDetails for one device by id.
curl -X GET "https://androidtexter.com/api/devices/:id" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "device": { "id": "...", "name": "...", "online": true } }Get an HTTP callback the moment a reply arrives, instead of polling.
/api/webhooksReturns all configured webhook endpoints.
curl -X GET "https://androidtexter.com/api/webhooks" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "webhooks": [{ "id": "...", "url": "https://...", "events": ["message.received"], "active": true }] }/api/v1/webhooksRegister a webhook URL programmatically. Returns a signing secret used to verify payloads; it is shown once.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| url | body | yes | https://your-server.com/webhook |
| events | body | yes | ["message.received"] |
curl -X POST "https://androidtexter.com/api/v1/webhooks" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"events": "[\"message.received\"]"
}'Example response
{
"webhook": { "id": "uuid", "url": "https://...", "active": true },
"secret": "whsec_..."
}/api/webhooks/:idRemove a webhook endpoint.
curl -X DELETE "https://androidtexter.com/api/webhooks/:id" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "message": "Webhook deleted" }Saved message shortcuts, shared between the dashboard and the phone app.
/api/quick-repliesReturns all quick reply shortcuts for the authenticated user.
curl -X GET "https://androidtexter.com/api/quick-replies" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "quick_replies": [{ "id": "...", "shortcut": "thanks", "body": "Thank you!", "usage_count": 5 }] }/api/quick-repliesCreate a new quick reply shortcut.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| shortcut | body | yes | thanks |
| title | body | yes | thanks |
| body | body | yes | Thank you for your message! We'll get back to you shortly. |
curl -X POST "https://androidtexter.com/api/quick-replies" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shortcut": "thanks",
"title": "thanks",
"body": "Thank you for your message! We'll get back to you shortly."
}'Example response
{ "quick_reply": { "id": "uuid", "shortcut": "thanks", "body": "..." } }/api/quick-replies/:idUpdate an existing quick reply.
Parameters
| Name | In | Required | Example |
|---|---|---|---|
| shortcut | body | yes | thanks |
| title | body | yes | thanks |
| body | body | yes | Updated message body |
curl -X PUT "https://androidtexter.com/api/quick-replies/:id" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shortcut": "thanks",
"title": "thanks",
"body": "Updated message body"
}'Example response
{ "quick_reply": { "id": "...", "shortcut": "thanks", "body": "Updated message body" } }/api/quick-replies/:idRemove a quick reply shortcut.
curl -X DELETE "https://androidtexter.com/api/quick-replies/:id" \
-H "X-API-Key: YOUR_API_KEY"Example response
{ "message": "Quick reply deleted" }When a message arrives on a connected device we POST a JSON payload to your configured webhook URL. Register one with Create Webhook above, or on the dashboard.
message.receivedFired when an SMS is received on any connected device.
{
"event": "message.received",
"timestamp": "2026-03-11T20:26:47Z",
"data": {
"id": "uuid",
"from": "+14155551234",
"body": "Hello!",
"device_id": "uuid",
"device_name": "Pixel 7",
"received_at": "2026-03-11T20:26:47Z"
}
}Each webhook is signed with your webhook secret. Verify the X-Webhook-Signature header using HMAC-SHA256 over the raw request body, and compare with a constant-time function.
import hmac, hashlib
def verify_webhook(payload_bytes, signature, secret):
expected = hmac.new(
secret.encode(),
payload_bytes,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)Rate limits are token buckets with a sustained rate and a burst allowance, counted per API key (or per user for JWT requests), not per device or per IP.
| Scope | Sustained | Burst | Notes |
|---|---|---|---|
| POST /api/messages/send and POST /api/messages/broadcast | 10 requests/second | 20 | Counted per API key, not per device. Sending from several devices on one key shares this bucket. |
| Everything under /api/v1/ | 5 requests/second (300/minute) | 60 | The large burst is deliberate: polling clients bunch their requests, and a tight burst was returning 429 to callers whose average rate was well under the limit. |
| POST /api/auth/login, register and MFA verify | 3 requests/second | 5 | Counted per IP address, since there is no authenticated identity yet. |
Exceeding a bucket returns 429 with { "error": "Rate limit exceeded. Try again later." }. Retry after a short back-off.
Errors are returned as JSON with an error field and a matching HTTP status.
| Status | Body | Meaning |
|---|---|---|
| 401 | { "error": "Invalid API key" } | The key is missing, malformed, revoked, or you sent the 10-character display prefix instead of the full key. |
| 400 | { "error": "..." } | The request body failed validation. The message names the field at fault. |
| 404 | { "error": "..." } | The addressed record does not exist, or does not belong to your account. |
| 429 | { "error": "Rate limit exceeded. Try again later." } | You exceeded a rate limit bucket. Back off and retry. |