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_…
EndpointScopePurpose
GET /v1/health
Service check
GET /v1/numbers
numbers:readYour usable numbers
POST /v1/messages
messages:sendSend to one or more numbers
GET /v1/messages
messages:readList messages
GET /v1/messages/{id}
messages:readOne message
DELETE /v1/messages/{id}
messages:sendCancel a queued message
GET /v1/groups
groups:readList groups
POST /v1/groups/{id}/messages
messages:sendBroadcast 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-Id header — 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

GET /v1/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

GET /v1/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

POST /v1/messages

Scope messages:send.

FieldDescription
torequiredA phone number, or a list of up to 100.
bodyrequiredMessage text (up to 1,600 characters).
fromoptionalA 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_refoptionalYour own reference, echoed back (≤ 80 chars).
ttloptionalSeconds the message stays queued before it expires (60–604800).
idempotency_keyoptionalSee 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

GET /v1/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

GET /v1/messages/{id}

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

DELETE /v1/messages/{id}

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.

GET /v1/groups
GET /v1/groups/{id}
POST /v1/groups/{id}/messages
# 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:

POST /v1/groups
POST /v1/groups/{id}/members
DELETE /v1/groups/{id}/members/{member_id}
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:sendSend messages and cancel queued ones.
messages:readRead messages and delivery status.
numbers:readList your numbers.
groups:readRead groups and members.
groups:writeCreate 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": "…"}}.

StatusCodeMeaning
400invalid_number, invalid_body, bad_requestThe request didn't validate.
401unauthorized, invalid_keyMissing, wrong, or revoked key.
402subscription_inactive, daily_limit_reachedPlan doesn't include SMS, or all recipients were over the send allowance (with a reason).
403insufficient_scopeThe key lacks the required permission.
404not_foundNo such message, group, or route.
409not_cancellable, no_number, group_inactiveValid request, wrong state.
429rate_limitedPer-minute request limit; see Retry-After.