Sending
Transactional SMS: OTPs, alerts and receipts
Transactional SMS carries one person's own event - an OTP, a payment alert, a receipt. Each request has exactly one recipient and an Idempotency-Key, and a live request names an approved template, an event_type and a unique event_reference, with message set to the template's text filled in.
When to use the transactional route
Use transactional for a message one person needs because of something they did or something that happened to their account. Anything sent to a list - offers, reminders to everyone, announcements - is promotional.
Transactional SMS is sent through the API only; the Console sends promotional SMS. It is sent at any hour, and it is claimed ahead of promotional traffic, so an OTP never waits behind a campaign.
One-time setup
- 1
Request a Transactional Sender ID
In the Console under Sender IDs, on the transactional route. A Sender ID approved only for promotional cannot send transactional SMS.
- 2
Submit a template for each event
Under Developers > Transactional templates: choose the Sender ID and the event type, and write the fixed wording with the allowed variables. Each template shows its Template ID.
- 3
Wait for approval
Sendozi reviews each template and activates the Sender ID's transactional profile with its approved templates and its recipient limits per minute and per day, shown on the same page.
| event_type | Allowed variables |
|---|---|
| otp | [code], [minutes] |
| payment_alert | [amount], [reference], [balance], [merchant] |
| security_alert | [time], [device], [location] |
| receipt | [amount], [reference], [balance], [merchant] |
| service_notification | [reference], [time], [name] |
The request
| Field or header | Required | Rule |
|---|---|---|
| route | Yes | "transactional" (or "otp"; sms_type is accepted in place of route). |
| sender | Yes | Your Transactional Sender ID, spelt as approved. |
| recipient | Yes | Exactly one number. An array, a comma-separated list or a repeated number returns 400 transactional_bulk_not_allowed. |
| message | Yes | The approved template with its variables replaced by your values. Sendozi sends this text as it is; it does not fill the template for you. |
| template_id | Live | The Template ID of an approved template on this Sender ID's profile. |
| event_type | Live | The template's event type: otp, payment_alert, security_alert, receipt or service_notification. |
| event_reference | Live | Your own unique id for this event, up to 128 characters. Each reference can be sent once per workspace. |
| Idempotency-Key header | Yes | Missing, the request returns 400 transactional_idempotency_required. |
# Approved template tpl_4b9c...: "Your Acme code is [code]. It expires in [minutes] minutes."
curl -X POST https://api.sendozi.com/v1/sms/send \
-H "Authorization: Bearer $SENDOZI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: login-user_42-attempt_3" \
-d '{
"route": "transactional",
"sender": "AcmeAlerts",
"recipient": "08012345678",
"message": "Your Acme code is 492811. It expires in 10 minutes.",
"template_id": "tpl_4b9c2d1e0f3a5b6c",
"event_type": "otp",
"event_reference": "login-user_42-attempt_3"
}'const template = "Your Acme code is [code]. It expires in [minutes] minutes.";
export async function sendLoginCode({ phone, code, attemptId }) {
const message = template.replace("[code]", code).replace("[minutes]", "10");
const response = await fetch("https://api.sendozi.com/v1/sms/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDOZI_API_KEY}`,
"Content-Type": "application/json",
// One key and one event reference per attempt, stable across retries.
"Idempotency-Key": `login-${attemptId}`,
},
body: JSON.stringify({
route: "transactional",
sender: process.env.SENDOZI_TRANSACTIONAL_SENDER,
recipient: phone,
message,
template_id: process.env.SENDOZI_OTP_TEMPLATE_ID,
event_type: "otp",
event_reference: `login-${attemptId}`,
}),
});
const body = await response.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.data.message_id;
}How the message is matched to the template
- The fixed wording must be identical, character for character, including punctuation and spacing.
- Each [variable] (or {{variable}}) may be replaced by any text of one or more characters.
- A message that does not match returns 403 transactional_template_mismatch before anything is reserved or sent.
Retries and resends
- A retry of the same event
- Resend the identical request with the same Idempotency-Key. Sendozi returns the original response, including the same message_id, and sends nothing new.
- A new code for the same person
- That is a new event: use a new Idempotency-Key and a new event_reference. Reusing an event_reference returns 409 transactional_event_conflict.
- A request that was refused
- If a send is refused - an empty wallet, a template mismatch - its event_reference is not used up, and the Idempotency-Key is released, so the corrected request can use both again.
Limits
Each profile has a recipient limit per minute and per rolling 24 hours, set when it is activated. The minute limit counts sends on that Sender ID; the daily limit counts all of the workspace's transactional sends in the last 24 hours. Past the minute limit a request returns 429 rate_limit_exceeded with a Retry-After header; past the daily limit, 429 transactional_rate_limit_exceeded. The workspace-wide limit of 300 send requests a minute applies as well.
In sandbox
A sandbox transactional send still needs one recipient and an Idempotency-Key, but template_id, event_type and event_reference are not checked and the Sender ID's profile is not consulted. Test the live rules with one real send to your own phone before launch.
Transactional errors
| Code | Status | Fix |
|---|---|---|
| transactional_idempotency_required | 400 | Send an Idempotency-Key header |
| transactional_bulk_not_allowed | 400 | Send exactly one recipient |
| invalid_transactional_event_type | 400 | Use one of the five event types |
| invalid_transactional_event_reference | 400 | Send event_reference, up to 128 characters |
| transactional_template_required | 400 | Send the template_id of an approved template |
| transactional_route_not_configured | 403 | The Sender ID has no active transactional profile yet |
| transactional_template_not_approved | 403 | The template or event type is not approved on this profile |
| transactional_template_mismatch | 403 | Send the template's exact wording with only the variables replaced |
| transactional_event_conflict | 409 | This event_reference was already sent; use a new one for a new event |
| rate_limit_exceeded | 429 | Minute limit reached; wait for Retry-After |
| transactional_rate_limit_exceeded | 429 | Daily limit reached; wait, or ask for a reviewed increase |
Frequently asked questions
- Can I send an OTP from the Sendozi Console?
- No. The Console sends promotional SMS. OTPs and other transactional SMS are sent through the API.
- Does Sendozi fill in my template variables?
- No. You send the finished text in message and the template_id it came from. Sendozi checks that the text matches the approved template and sends it as written.
- Are transactional messages delivered at night?
- Yes. The 08:30 to 19:30 WAT window applies only to promotional SMS.
Related reading
Sending
Sending SMS
POST /v1/sms/send and /v1/sms/bulk: every request field, the response, how recipients are cleaned, limits, the delivery window, and exactly how a send is priced and charged.
Reliability
Idempotency and safe retries
How the Idempotency-Key header works on Sendozi send endpoints, what a repeat returns, why the body must be byte-for-byte identical, how long keys last, and how to choose one.
Reliability
Errors and the response envelope
The Sendozi response envelope, every error code the API returns with its HTTP status and fix, which errors are safe to retry, Retry-After, and how request_id traces a request.
Sending
Messages, batches and delivery status
Every send creates a batch of recipients, each becoming a message. How to look them up, what each status means, why a message failed, how to page through lists, and how to cancel a send.