Skip to content
Sendozi

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.

By SendoziUpdated 4 min read

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. 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. 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. 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 types and the variables each template may use
event_typeAllowed 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]
Event types and the variables each template may use

The request

Field or headerRequiredRule
routeYes"transactional" (or "otp"; sms_type is accepted in place of route).
senderYesYour Transactional Sender ID, spelt as approved.
recipientYesExactly one number. An array, a comma-separated list or a repeated number returns 400 transactional_bulk_not_allowed.
messageYesThe 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_idLiveThe Template ID of an approved template on this Sender ID's profile.
event_typeLiveThe template's event type: otp, payment_alert, security_alert, receipt or service_notification.
event_referenceLiveYour own unique id for this event, up to 128 characters. Each reference can be sent once per workspace.
Idempotency-Key headerYesMissing, the request returns 400 transactional_idempotency_required.
A login OTP
cURL
# 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"
  }'
Node.js
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

CodeStatusFix
transactional_idempotency_required400Send an Idempotency-Key header
transactional_bulk_not_allowed400Send exactly one recipient
invalid_transactional_event_type400Use one of the five event types
invalid_transactional_event_reference400Send event_reference, up to 128 characters
transactional_template_required400Send the template_id of an approved template
transactional_route_not_configured403The Sender ID has no active transactional profile yet
transactional_template_not_approved403The template or event type is not approved on this profile
transactional_template_mismatch403Send the template's exact wording with only the variables replaced
transactional_event_conflict409This event_reference was already sent; use a new one for a new event
rate_limit_exceeded429Minute limit reached; wait for Retry-After
transactional_rate_limit_exceeded429Daily 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.

Make retries safe

Pair every transactional send with a stable Idempotency-Key.