Authentication
Create a free account, pair a phone, and make a key on the API keys page and send it as the x-api-key header, or as Authorization: Bearer. Base URL:
https://sparetext.dev/api/v1Endpoints
/messagesSend a text
Queues the text on your default phone (or the one you name) and wakes it. Up to 100 recipients and 1,600 characters. One SMS holds 160 GSM-7 characters, or 153 per segment when it's longer; an emoji or any character outside GSM-7 makes it UCS-2, with 70 (or 67 per segment). Carriers bill each segment.
- to
- string or string[], up to 100. E.164 numbers, like +447700900124. A number without a country code is read in your account's country (Settings › Phone numbers) and refused if none is set. Numbers that replied STOP are skipped and listed in skipped.
- text
- string. The message.
- deviceId
- optional. Which phone sends it.
- scheduledAt
- optional ISO time, up to 30 days ahead.
- ttl
- optional whole seconds, 10 to 30 days. If the text hasn't gone out this long after it may (scheduledAt, or now), it is dropped unsent and fails with error expired. For one-time codes. Or give expiresAt, an ISO time, instead.
- Idempotency-Key
- optional header. Any unique string; a retry with the same key within 24 hours gets the first answer back instead of texting again.
curl -X POST https://sparetext.dev/api/v1/messages \
-H "x-api-key: $SPARETEXT_API_KEY" \
-H "content-type: application/json" \
-d '{"to": "+15551234567", "text": "Your code is 481920", "ttl": 300}'{
"batchId": "b_x1…",
"messages": [{ "id": "msg_20261027_k3j9x2ab", "contact": "+15551234567", "status": "queued", … }]
}/messages/{id}Check a text
Status moves queued → dispatched (shown as Sending: the phone has it) → sent → delivered, or failed with the phone's reason in error. Only Android phones get delivery reports, so on iPhone sent is final.
curl https://sparetext.dev/api/v1/messages/msg_20261027_k3j9x2ab \
-H "x-api-key: $SPARETEXT_API_KEY"{ "message": { "id": "msg_…", "status": "delivered", "sentAt": "…", "deliveredAt": "…" } }/messagesList texts
Newest first. Page with nextCursor.
- direction
- inbound or outbound
- contact
- only texts with this number
- status
- queued, sent, delivered, failed, received…
- q
- search the text
- since
- ISO time; only newer texts
- limit
- 1 to 100, default 50
- before
- the nextCursor from the last page
curl "https://sparetext.dev/api/v1/messages?direction=inbound&limit=20" \
-H "x-api-key: $SPARETEXT_API_KEY"{ "messages": [ … ], "nextCursor": "MjAyNi0xMC0y…" }/messages/{id}Cancel a text
Stops a text that is still scheduled or waiting for the phone. Once the phone has taken it, it can't be called back (409).
curl -X DELETE https://sparetext.dev/api/v1/messages/msg_20261027_k3j9x2ab \
-H "x-api-key: $SPARETEXT_API_KEY"204 No Content/devicesList phones
Each phone with online, battery, queue and mode.
curl https://sparetext.dev/api/v1/devices -H "x-api-key: $SPARETEXT_API_KEY"{ "devices": [{ "id": "dev_…", "name": "Pixel 8", "platform": "android", "online": true, "queued": 0 }] }/gateway/send-smstextbee-compatible send
Same body as textbee's send-sms, so existing code can switch by changing the base URL and key.
curl -X POST https://sparetext.dev/api/v1/gateway/send-sms \
-H "x-api-key: $SPARETEXT_API_KEY" -H "content-type: application/json" \
-d '{"recipients": ["+15551234567"], "message": "Hello"}'{ "data": { "batchId": "b_…", "messages": [ … ] } }Webhooks
We POST JSON to your URL for message.received, message.sent, message.delivered, message.failed, device.paired, and device.offline and device.online (an Android phone or Mac relay that stops checking in, and its return; the phone's last status says why). Each request carries X-Sparetext-Signature: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of t.body with the webhook's secret. Check it over the raw body, and refuse a t older than 5 minutes.
Answer with any 2xx within 5 seconds, then do the work. Anything else is tried again: after 5 seconds, then 5 and 30 minutes, then 2, 5, 10 and 10 hours, 8 tries in all. Every try sends the same body with the same id, signed afresh, and X-Sparetext-Attempt counts them, so skip an id you have already handled. Events can arrive out of order.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as it arrived (a string or Buffer).
export function fromSparetext(rawBody, header, secret) {
const sig = Object.fromEntries(String(header ?? "").split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${sig.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
return fresh && String(sig.v1).length === expected.length && timingSafeEqual(Buffer.from(String(sig.v1)), Buffer.from(expected));
}Android's sending limit
Android lets an app send 30 SMS parts a minute (a long text counts each part). Over that it stops to ask the person holding the phone, so Sparetext paces under it and texts beyond it wait, queued, for the next minute. To send faster from a phone you own, raise the limit over USB with adb, then restart the phone:
adb shell settings put global sms_outgoing_check_max_count 1000Carriers watch volume too: hundreds of texts an hour from one SIM can get it filtered or blocked, whatever Android allows.
Errors
Every error has a status, a stable code and a sentence you can show to people.
409 {"error": {"code": "no_device", "message": "Connect a phone first: Phones > Add phone."}}400 bad_requestSomething in the body is wrong; the message says what.401 unauthorizedMissing, wrong or revoked key.404 not_foundNo such text, phone or endpoint.409 no_deviceNo phone is on to send it.429 rate_limitedSlow down and retry.500 internalOur side. Retry with backoff.