SMS API
Send SMS from your own app, one at a time or many at once, and read back where each one got to.
Your app sends SMS through luftflow over HTTPS with a key, and every message shows under Messaging → Messages with where it got to.
Keys
An admin makes a key under Messaging → API. It is shown once, so copy it then and keep it in your app's secrets. A key belongs to the team, not to whoever made it, and it reaches the SMS API alone. Make one per app, so revoking one stops only that app.
Send it as a bearer token on every request:
Authorization: Bearer lftk_…
Send one message
curl https://luftflow.com/api/sms/messages \
-H "Authorization: Bearer $LUFTFLOW_KEY" \
-H "Idempotency-Key: order-123" \
-d from=YOURSENDER \
-d to="+<country code><number>" \
-d text="Your order is on its way"
| Field | |
|---|---|
from |
One of your team's senders, as it is named under Messaging → Senders |
to |
The phone number with its country code, starting with + |
text |
Up to 10 parts. A part is 160 characters, or 70 with emoji or letters outside the GSM alphabet |
ref |
Optional. Your own reference; it comes back on the message and you can search for it |
validity_minutes |
Optional. How long the network keeps trying, 1 to 4320; one day if left out |
transactional |
Optional. true for a message the person asked for, such as a passcode: it reaches them even if they opted out |
A message that is taken is answered 202 with the message, its id and its status. Send the same Idempotency-Key again and you get the first message back with 200 instead of sending twice: use one key per message you mean to send, such as your order or passcode id.
Read where it got to
curl https://luftflow.com/api/sms/messages/ID \
-H "Authorization: Bearer $LUFTFLOW_KEY"
status moves from queued to submitted (sent to the network), accepted (the network took it) and delivered, or ends failed or expired, with error saying why. receipts holds each part's own status. The answer never carries the text or the whole number.
Send to many people at once
POST /api/sms/batches with from, text and recipients, a list of { "to": "+…", "ref": "…", "fields": { … } }. A {{ name }} in the text is filled from each person's fields. Add send_at (ISO 8601) to schedule it, or "start": false to keep it a draft and add more people with POST /api/sms/batches/ID/recipients. Each person who cannot be sent to is listed in refused with the reason, and the rest go.
GET /api/sms/batches/ID answers its progress, and /start, /pause, /resume and /cancel steer it.
Opt-outs
A person who replies STOP is opted out of your team's SMS. GET /api/sms/suppressions lists them, POST with phone adds one and DELETE with phone lifts one. channel is sms unless you say whatsapp or all.
Credit
A team on prepaid credit pays for each SMS from its credit. The price of each part is held when luftflow takes a message, spent when it is delivered, and given back when it is not, so you pay only for what arrives. Messaging → API shows what is left and the price a part on each network. When the credit runs out, a message is refused with no_credit and a batch pauses until someone presses Resume.
An admin adds credit there with mobile money or a card; it is added once the payment is confirmed, and a receipt is emailed. Your app can read what is left:
curl https://luftflow.com/api/sms/balance \
-H "Authorization: Bearer $LUFTFLOW_KEY"
It answers { "prepaid": true, "currency": "USD", "available_cents": 1234, "held_cents": 40 }.
Refusals and limits
A message luftflow will not send is answered 422 with status: "rejected", a reason your code can read and an error a person can:
| Reason | |
|---|---|
unknown_sender |
from is none of your senders |
not_a_number |
to is not a phone number with its country code |
no_network / no_route |
luftflow does not send to that number's network yet |
sender_not_registered |
the sender is not registered on that network yet |
suppressed |
the number opted out, and the message is not transactional |
too_long / empty_text / bad_validity |
the text or validity is out of bounds |
no_credit |
a prepaid team's credit does not cover the message; credit in the answer says what is left |
no_rate |
a prepaid team cannot pay for that network yet |
Each key may make 300 requests a minute; past that it is answered 429. For more than a few hundred people at once, use a batch.