Skip to content
luftflow

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.