Send a transactional SMS
A transactional SMS is sent one at a time, when your system decides: a booking confirmation, an appointment reminder, a pickup code. It is billed at the same rate as your campaigns and counts towards the same daily cap.
Use this API only for messages that respond to an action of the recipient. Promotional messages (offers, sales follow-ups) go through campaigns, which handle the STOP mention and unsubscribes. A recipient who unsubscribed from your campaigns still receives your transactional messages.
Prerequisites
- A secret API key with the
send_transactional_messagespermission (andview_transactional_messagesto read sent messages). See Authentication. - Credits, or an SMS allowance included in your plan.
- Optional: a verified SMS sender (Settings › Senders). Without one, the message is sent from the platform's short code.
Send
curl https://api.metaventus.com/v1/transactional/sms \
-H "X-API-Key: YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-20481" \
-d '{
"to": "+33612345678",
"from": "LEBISTROT",
"body": "Your table for 4 is confirmed tonight at 8pm. Thank you!",
"reference": "booking-20481"
}'
| Field | Required | Description |
|---|---|---|
to | yes | Recipient number in international format (+33612345678). A national number is read as French. |
body | yes | The text, sent as is. 9 SMS at most (see Length and cost). |
from | no | A verified SMS sender of your account. Without from: your default sender, otherwise the platform's. |
reference | no | Your own identifier (booking number…), up to 128 characters. Returned as is, filterable. |
Response 202 Accepted: the message is accepted, billed and on its way.
{
"id": "922c2bfd-32ec-4773-b348-63967731e3a7",
"channel": "sms",
"to": "+33612345678",
"from": "LEBISTROT",
"body": "Your table for 4 is confirmed tonight at 8pm. Thank you!",
"segments": 1,
"encoding": "gsm7",
"country_code": "33",
"status": "queued",
"reference": "booking-20481",
"created_at": "2026-10-14T09:00:00Z",
"updated_at": "2026-10-14T09:00:00Z"
}
202 is not an error: it is the normal response to a send.
Never send twice
Send an Idempotency-Key header unique to each message (your booking number, for
instance). If your request times out and you retry with the same key, the first message is
returned with a 200 and the Idempotent-Replayed: true header: nothing is sent again
to the recipient, nothing is billed again.
Reusing a key for a different message (other recipient or other text) is rejected
(422 idempotency_key_reused).
Length and cost
An SMS is billed by the number of SMS the carrier transports, at the rate of the
recipient's country — the same as your campaigns. The response shows what was counted
(segments, encoding, country_code).
| Alphabet | 1 SMS | Beyond, per SMS |
|---|---|---|
GSM-7 (Latin letters, é, è, à…) | 160 characters | 153 characters |
Unicode (as soon as one character is outside GSM-7: ç, ê, â, ô, À, emoji…) | 70 characters | 67 characters |
The characters € [ ] { } ^ ~ | \ count twice in GSM-7. A message is limited to
9 SMS; beyond that it is rejected (400 body_too_long) and nothing is billed.
Track delivery
curl https://api.metaventus.com/v1/transactional/messages/922c2bfd-32ec-4773-b348-63967731e3a7 \
-H "X-API-Key: YOUR_SECRET_KEY"
status | Meaning |
|---|---|
queued | Accepted and billed, being handed to the carrier. |
sent | Handed to the carrier (sent_at). |
delivered | Received by the handset (delivered_at). |
failed | Not delivered — see error_code. |
error_code | Cause | Billed |
|---|---|---|
rejected | Rejected by the carrier (number or content). | No, refunded |
not_sent | Never handed to the carrier. | No, refunded |
undelivered | Transported but not received (handset off, out of coverage, inactive number). | Yes |
The delivery receipt usually arrives within seconds; the carrier retries delivery for up to 48 hours. The status is refreshed about every two minutes: poll it sparingly.
List messages
curl "https://api.metaventus.com/v1/transactional/messages?status=failed&limit=50" \
-H "X-API-Key: YOUR_SECRET_KEY"
Filters: status, reference, to. Messages come newest first, limit from 1 to 100
(default 50). When has_more is true, request the next page with
starting_after=<id of the last message received>.
Rejections
A rejected request is neither sent nor billed. The body carries a stable code to
branch your logic on, and a human-readable sentence in error.
| Status | code | Cause | What to do |
|---|---|---|---|
400 | invalid_to, invalid_body, body_too_long, invalid_reference | Invalid request | Fix it |
402 | insufficient_credits | Insufficient balance (details.credits_needed, details.wallet_balance) | Top up, then retry |
403 | account_suspended | Subscription suspended | Settle it |
422 | sender_not_allowed | from is not a verified sender of the account | Fix from or remove it |
422 | idempotency_key_reused | Key already used for another message | New key |
429 | daily_limit_reached | Your plan's SMS cap is reached for the period (details.limit, details.used) | Wait for the next day, or upgrade |
503 | temporarily_unavailable | Transient incident | Retry with the same Idempotency-Key |
{
"error": "Plafond de votre offre atteint : 100 SMS par jour, il en reste 1, ce message en compte 2.",
"code": "daily_limit_reached",
"details": { "limit": 100, "used": 99, "period": "daily", "segments": 2 }
}
The error sentence is in French; rely on code.
The daily cap is shared with your campaigns, automations and appointment reminders: there is one SMS allowance, whatever the sending channel.