Developers · SMS
SMS API: send a text, prove it arrived
Quotes, job updates, reminders and campaigns from your own software. One call to send, a delivery receipt for every message, STOP handled for you. Base URL https://callbullet.com/api/public/v1.
Build it with AI
Paste this into ChatGPT, Claude, Lovable or Cursor inside your app's project.
You are adding SMS to my app using Call Bullet (UK business texting with delivery proof).
Docs: https://callbullet.com/developers/api/sms · Spec: https://callbullet.com/openapi.json
Base URL https://callbullet.com/api/public/v1 · Auth: Authorization: Bearer <key>, server-side only (env CALL_BULLET_API_KEY). Never call it from the browser.
Ask me ONLY for: the API key (the "Full SMS app" preset covers every brand it is scoped to) and my test mobile. Everything else you discover.
1. GET /sms/workspaces → every brand this key can use, with credits, senders, templates, live_ready and suggested_sender_names. Never guess IDs.
2. For each brand: GET /sms/readiness?workspace_id=… . Follow next_step. Show me human_actions in plain words (buy credits, verify mobile, consent) with their links.
3. No sender → POST /sms/senders {workspace_id, kind:"alphanumeric", value, use_case, sample_message}. Names are 3–11 letters/numbers/spaces; on 422 invalid_sender_name use error.suggestions and tell me what you picked. New senders are "pending" until Call Bullet approves (usually one working day). Sender names can't take replies.
4. Templates → GET /sms/templates; create/update with POST /sms/templates and PATCH /sms/templates/{id} using {{customer_name}}, {{job_ref}} variables. 409 = exists → PATCH. Keep under 160 plain characters.
5. Simulated test now → POST /sms/messages with "test":true, template_id and realistic demo variables. Show me the rendered body, segments and timeline. Nothing is sent, no credits.
6. Real test to ANY number the owner chooses, free, even while the sender is pending → POST /sms/test-mobiles {workspace_id, phone}; ask for the 6-digit code that number receives; POST /sms/test-mobiles/verify {workspace_id, phone, code}. Then send with "test":"live". Pending senders arrive from "CallBullet" prefixed "[Test] <Brand>:". Max 20 a day per brand. Never send live to customers until the owner has reviewed every template.
7. Webhooks → POST /sms/webhooks {workspace_ids:[…], url:"https://<my app>/api/call-bullet", events omitted = all}. Store signing_secret (shown once); verify X-Call-Bullet-Signature = "v1=" + HMAC-SHA256(secret, X-Call-Bullet-Timestamp + "." + raw body). Handle sms.sent, sms.delivered, sms.failed, sms.clicked, sms.sender.approved (resume go-live automatically), sms.sender.rejected (tell me the reason).
8. Production sendSms({workspace, to, templateId, variables, reference, tags, campaign}) → POST /sms/messages with Idempotency-Key = stable reference ("job-1042-booked"), external_ref = my job/lead ID, metadata {job_id, lead_id, customer_id, customer_name}, tags, campaign, track_links:true when there is a link. Store the returned Call Bullet message id on my job/lead record. Normalise UK numbers to +44. Only send live once readiness.live_ready is true (every workspace includes 25 free credits; live texts need an approved sender).
9. Errors carry next_step and sometimes human_action: 402 insufficient_credits, 422 sender_not_ready | opted_out | compliance_blocked | missing_variables | invalid_phone, 429 → Retry-After. 403 forbidden with missing_permission → existing keys can be upgraded in place; do not ask for a new key. Ask me to click Edit permissions on the same key, choose Full SMS app and Save, then retry.
## Build an SMS accountability page in my app (single source of truth)
Make it unforgivingly simple and detailed. Plain English, no jargon, times in Europe/London.
- Workspaces: if the key covers more than one brand, group, location or workspace, add a switcher (tabs for a few, a dropdown/search for many; hide it when there is only one). Never mix workspaces' data. Adapt to how my app is structured — don't assume brands.
- Inside each workspace, sub-sections:
a. Sender name — the name customers see, its status (pending / approved / rejected + reason) and what that means, and whether replies are possible.
b. Templates — every template with a rendered preview using sample variables, characters, segments/credits per send, category, and a "Send test" button (any number, verified by code as in step 6).
c. Messages log — every text, newest first, searchable by phone number, job/lead reference, customer, campaign, template, status and date (GET /sms/messages?workspace_id&to&external_ref&campaign&template_id&tag&status&from&to_date&test=all). Each row: date/time, recipient, linked job/lead/customer in my app (link to it), template, sender, status badge, credits_used. Clicking opens the timeline from GET /sms/messages/{id} (queued → sent → delivered/failed, clicks, linked callbacks) with error reasons in plain words. Label tests clearly.
d. Usage & credits — current balance, credits used this period, texts / delivered / failed / delivery rate, breakdowns by day, template and campaign from GET /sms/usage?workspace_id&from&to_date&group_by=day|template|campaign|status|tag|sender, and the full credit history (top-ups, starter grant, sends, refunds with running balance) from GET /sms/credits/ledger?workspace_id&page. Warn when the balance is low. CSV export for the log, usage and ledger.
- On every job/lead/customer page, show the texts linked to it (filter by external_ref).
- Keep it in sync with webhooks plus a fallback sync every few minutes using updated_since.
10. Finish with a table per workspace: sender + status, templates, simulated test, live test result (delivered?), webhook id, accountability page URL, and what I still need to do.Starting from an empty account
- Your app can do: list brands, check readiness, request sender names, create templates, send simulated and real test texts, register webhooks, send live texts, and read delivery, clicks and call-backs.
- Only a person can do: create the API key, read out the test-mobile code, buy credits beyond the free 25, add a reply-capable number and record marketing consent. Call Bullet staff approve sender names; your app hears about it through the sms.sender.approved webhook.
- Missing a permission? Existing keys can be upgraded in place; do not create a new key. The workspace owner opens Integrations → Developer API, clicks Edit permissions on the key, chooses Full SMS app and saves. The readiness check's
permissionsstep lists exactly what is missing. - Sender name length: 3–11 characters including spaces. “Car Specialist Customs” is too long; use “CSC”. “Wheels Clinic” (13) becomes “WheelsClinc” and “Ghost Fitters” becomes “GhostFitter”.
- Test mode:
"test": trueruns every check and renders the template, but nothing is sent."test": "live"sends a free real text to a verified test mobile, even while your sender is pending (from “CallBullet”, prefixed “[Test] Brand:”). - Free starter credits: every workspace includes 25 free credits from the start; live texts still need an approved sender.
- Attribution: set
external_ref(your job ID),tags,campaignandtrack_links. Link clicks are logged, and a call from the same mobile to a tracked number within 7 days is linked to the text.
Before you start
- In Call Bullet, open SMS → Get started and add a sender. A sender name (e.g. “GoTints”, 3–11 letters) looks professional but can't receive replies. A Call Bullet number can receive replies. Your personal mobile can't be used as a sender — UK networks block it.
- Buy credits on SMS → Credits. 1 credit = 1 UK segment.
- Create a key in Integrations → Developer API and choose the SMS app preset (send, senders, templates) or set SMS Messages/Senders/Templates to Write.
Send a text
curl -X POST https://callbullet.com/api/public/v1/sms/messages \
-H "Authorization: Bearer $CALL_BULLET_API_KEY" \
-H "Idempotency-Key: quote-48213" \
-H "Content-Type: application/json" \
-d '{"workspace_id":"WORKSPACE_ID","sender_id":"SENDER_ID","to":"07700900123","body":"Hi Sam, your quote is ready: https://…"}'
# 202 → {"data":{"id":"…","status":"sent","segments":1,"credits":1}}
# Template + attribution: {"workspace_id":"…","sender_id":"…","to":"07700900123","template_id":"…",
# "variables":{"customer_name":"Sam","job_ref":"J-1042"},"external_ref":"job-1042","tags":["booking"],
# "campaign":"MOT reminders","track_links":true}
# Test mode: add "test":true → full checks, rendered text, simulated delivery, 0 credits, nothing sent.
# Errors: 402 insufficient_credits · 422 opted_out | compliance_blocked | sender_not_ready | invalid_requestRetrying with the same Idempotency-Key returns the original message (duplicate: true) — never a second text.
Endpoints
| GET | /sms/workspaces | Every brand this key can text for, with credits, senders, templates and suggested sender names. |
| POST | /sms/test-mobiles | Text a 6-digit code to a test mobile; POST /sms/test-mobiles/verify confirms it. Then send with test:"live" (free, 20/day). |
| POST | /sms/webhooks | Register a webhook for SMS events; returns the signing secret once. DELETE /sms/webhooks/{id} disables. Needs sms:webhooks:write. |
| GET | /sms/readiness?workspace_id=… | Start here: senders, approval, templates, credits, webhooks, live_ready, test_ready and next_step. |
| POST | /sms/messages | Send one text. Body: workspace_id, sender_id, to, body or template_id + variables, category, external_ref, tags, campaign, metadata, track_links, test. Header Idempotency-Key. Needs sms:send. |
| POST | /sms/senders | Request a sender name (3–11 chars; 422 returns suggestions) or attach a Call Bullet number. Starts pending. Needs sms:senders:write. |
| GET | /sms/senders/{id} | One sender with status and reason. |
| POST | /sms/templates | Create a template with {{variables}}. Needs sms:templates:write. |
| PATCH | /sms/templates/{id} | Update name, body or category. DELETE removes it. |
| GET | /sms/messages?workspace_id=… | List texts. Filters: status, to, external_ref, tag, campaign, template_id, test, from, to_date, updated_since, page, per_page (≤500). |
| GET | /sms/messages/{id} | One text plus carrier timeline, tracked links, clicks and calls from that mobile within 7 days. |
| GET | /sms/senders?workspace_id=… | Approved sender names/numbers and whether replies are allowed. |
| GET | /sms/templates?workspace_id=… | Saved templates. |
| GET | /sms/balance?workspace_id=… | Credits left. |
| GET | /sms/usage?workspace_id=…&group_by=day|template|campaign | Texts, delivered, failed, delivery rate and credits used for a period. |
| GET | /sms/credits/ledger?workspace_id=… | Every top-up, starter grant, send and refund with running balance. |
Delivery statuses
- queued — accepted by Call Bullet, waiting to send (automatic retries).
- sent — the mobile network accepted it.
- delivered — the handset confirmed receipt.
- failed / expired — not delivered; reason in
error; credits refunded when we couldn't send.
Prefer push to polling: add a webhook and receive sms.sent, sms.delivered and sms.failed the moment they happen — see the webhook guide.
Segments and credits
Plain text: 160 characters in one segment, 153 per segment after that. Any emoji or special character switches to Unicode: 70, then 67. Each segment is one credit in the UK; international texts are 3 credits per segment.
Rules we enforce
STOP replies opt the number out automatically (422 opted_out). Marketing texts need recorded consent, get “Reply STOP to opt out” added and are blocked 9pm–8am UK time. Transactional texts (quotes, job updates, reminders) don't need marketing consent.
Errors
402 insufficient_credits · 403 forbidden (scope or permission) · 422 opted_out, compliance_blocked, sender_not_ready, invalid_request · 429 rate_limited (Retry-After).
Main API reference: /developers/api