SMS API
Send and receive text messages, list your numbers, and manage group texting from your server. Unlike the other APIs, the SMS API is served by sms.bodek.us directly — not the dev gateway. You create and manage keys here on SMS API keys.
Base https://sms.bodek.us/api/v1
Auth Authorization: Bearer bs_live_…
| Endpoint | Scope | Purpose |
|---|---|---|
GET /v1/health | — | Service check |
GET /v1/numbers | numbers:read | Your usable numbers |
POST /v1/messages | messages:send | Send to one or more numbers |
GET /v1/messages | messages:read | List messages |
GET /v1/messages/{id} | messages:read | One message |
DELETE /v1/messages/{id} | messages:send | Cancel a queued message |
GET /v1/groups | groups:read | List groups |
POST /v1/groups/{id}/messages | messages:send | Broadcast to a group |
Authentication
Send your key as a bearer token. Keys are server-side credentials — never ship one in a
browser or mobile app. A revoked key, or an account whose plan no longer includes SMS, stops
working immediately (401 invalid_key / 402 subscription_inactive).
Authorization: Bearer bs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
A key can be bound to one number. A bound key can only send from — and only sees messages on — that number.
Requests & responses
- All requests and responses are JSON.
- Every response is an envelope:
{"ok": true, …}on success or{"ok": false, "error": {"code": "…", "message": "…"}}on failure. - Each response carries an
X-Request-Idheader — quote it in support requests. - Phone numbers are E.164 (
+15551234567); US 10-digit input is accepted and normalised. A message body is up to 1,600 characters (~10 segments). - A send returns 202 with the queued messages; poll GET /v1/messages/{id} for delivery status.
Rate limits
Each key has a per-minute request limit (default 30/min; set when you create
the key). Over it you get 429 rate_limited with a Retry-After header.
Separately, the account has a daily send allowance. Sends past it are rejected
unless the account has pay-as-you-go enabled (then they're metered up to a
spending cap). See Send a message for how rejections appear
(rejected/reason, or a 402 daily_limit_reached when all
recipients are rejected).
Health
No scope, but a valid key is required.
curl https://sms.bodek.us/api/v1/health -H "Authorization: Bearer bs_live_…"
{ "ok": true, "service": "sms", "version": "v1", "time": "2026-09-24T00:00:00+00:00" }
List numbers
Scope numbers:read. The numbers you can send from: the shared sending pool
(available to every account with SMS access) plus any group-dedicated number assigned to your
account. A key bound to one number returns just that number.
curl https://sms.bodek.us/api/v1/numbers -H "Authorization: Bearer bs_live_…"
{
"ok": true,
"numbers": [
{ "id": 12, "number": "+15551234567", "display": "(555) 123-4567", "label": "Front desk", "online": true }
]
}
Send a message
Scope messages:send.
| Field | Description | |
|---|---|---|
to | required | A phone number, or a list of up to 100. |
body | required | Message text (up to 1,600 characters). |
from | optional | A number id or E.164 to send from — any number in the shared pool or your group-dedicated number. Defaults to the pool. Ignored (forced) for a bound key. |
client_ref | optional | Your own reference, echoed back (≤ 80 chars). |
ttl | optional | Seconds the message stays queued before it expires (60–604800). |
idempotency_key | optional | See Idempotency (or send the Idempotency-Key header). |
curl -X POST https://sms.bodek.us/api/v1/messages \
-H "Authorization: Bearer bs_live_…" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"body": "Your table is ready!",
"client_ref": "order-8412"
}'
Returns 202:
{
"ok": true,
"messages": [
{ "id": 5567, "direction": "out", "phone": "+15551234567", "status": "queued",
"segments": 1, "client_ref": "order-8412", "created_at": "2026-09-24T00:00:00+00:00" }
]
}
Sending to a list of to numbers queues one message per distinct number.
Billing limits. If the account is over its daily send allowance and pay-as-you-go
is off, recipients are rejected. When some go through and some don't, the response is still
202 but adds rejected (a count), reason
(limit_reached or metered_cap_reached) and a human message.
If every recipient is rejected, the call returns
402 daily_limit_reached with the
reason — enable pay-as-you-go to send beyond the daily allowance.
Idempotency
To make a send safe to retry, set an Idempotency-Key header (or an
idempotency_key field), up to 80 characters. A repeat within 24 hours with the same
key returns the same messages instead of sending again — so a network timeout and retry
never double-texts a customer.
curl -X POST https://sms.bodek.us/api/v1/messages \
-H "Authorization: Bearer bs_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-8412-confirm" \
-d '{ "to": "+15551234567", "body": "Your table is ready!" }'
List messages
Scope messages:read. Query parameters: direction (in/out),
status, phone, number_id, group_id,
since_id, before_id, limit (default 50, max 200).
curl "https://sms.bodek.us/api/v1/messages?direction=in&limit=20" \
-H "Authorization: Bearer bs_live_…"
{ "ok": true, "messages": [ … ], "next_before_id": 5540 }
Page backwards with before_id, or poll new messages with since_id.
Get a message
Scope messages:read. Poll this for delivery status
(queued → sending → sent → delivered, or failed/expired).
curl https://sms.bodek.us/api/v1/messages/5567 -H "Authorization: Bearer bs_live_…"
{ "ok": true, "message": { "id": 5567, "status": "delivered", "delivered_at": "…", … } }
Cancel a message
Scope messages:send. Cancels a message that is still queued; once it's
handed to a phone it can't be recalled (409 not_cancellable).
curl -X DELETE https://sms.bodek.us/api/v1/messages/5567 -H "Authorization: Bearer bs_live_…"
Groups
A group lives on one number; a text from a member is forwarded to the others. Read your groups
with groups:read, broadcast with messages:send, and (optionally) manage
them with groups:write.
Groups need a dedicated number. Unlike one-off sends (which use the shared pool), a group must be hosted on a number specifically assigned to your account. Creating a group on a shared/pool number is rejected — request a dedicated number first.
# One message to everyone in the group:
curl -X POST https://sms.bodek.us/api/v1/groups/7/messages \
-H "Authorization: Bearer bs_live_…" \
-H "Content-Type: application/json" \
-d '{ "body": "Practice is cancelled tonight." }'
With groups:write you can also create groups and manage members:
curl -X POST https://sms.bodek.us/api/v1/groups \
-H "Authorization: Bearer bs_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "U12 Team",
"number_id": 12,
"members": [ { "name": "Coach", "phone": "+15551230001", "admin": true } ]
}'
System send (Bodek internal)
Bodek's own apps send 2FA codes and account notices through a separate, internal path
(POST /v1/system/send) authenticated by a shared server secret, not an API key.
It isn't available to customer keys. In-process callers use the Bodek\Sms\Internal
class instead. This is documented for completeness only.
Scopes
messages:send | Send messages and cancel queued ones. |
messages:read | Read messages and delivery status. |
numbers:read | List your numbers. |
groups:read | Read groups and members. |
groups:write | Create and change groups. |
Choose scopes when you create a key. A call without the needed scope
fails with 403 insufficient_scope.
Errors
Every error is {"ok": false, "error": {"code": "…", "message": "…"}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_number, invalid_body, bad_request | The request didn't validate. |
| 401 | unauthorized, invalid_key | Missing, wrong, or revoked key. |
| 402 | subscription_inactive, daily_limit_reached | Plan doesn't include SMS, or all recipients were over the send allowance (with a reason). |
| 403 | insufficient_scope | The key lacks the required permission. |
| 404 | not_found | No such message, group, or route. |
| 409 | not_cancellable, no_number, group_inactive | Valid request, wrong state. |
| 429 | rate_limited | Per-minute request limit; see Retry-After. |