Building an SMS Notification System with an API
Follow one SMS from the event in your app to the status in your database: the queue and retries that are yours to build, the send call and the delivery webhook that are documented.
- Stages From Event To Status
- 5
- Documented Webhook Events
- 2
- Code Samples
- 3
Read against the developer page and TRAI’s directions, 20 October 2026
- NotifyTech AI Team
- 7 min read
- Automation
Why an SMS needs more than one API call
Calling the SMS API straight from the code that handles an event works for a prototype. At scale one failed call or a traffic spike means a missed OTP or order alert. A queue, a retry policy and a stored status are what make the system recoverable.
This guide follows one message through five stages and marks which are yours and which are documented on our side. It is not legal advice: check your DLT portal for your own registrations.
The notification path at a glance
- Send
- POST /v1/sms/send, JSON over HTTPS, API key as a Bearer token
- Reports
- message.delivered and message.failed, by signed webhook
- Queue and retries
- Yours: no idempotency key is documented
- Promotional window
- TRAI’s default is 10:00 to 21:00
- Code samples
- Node.js, Python and PHP on the developer page
What does each kind of message need?
Decide the kind of message before you design the queue. A one-time code, an order alert and a campaign differ in how long they can wait, and each is tied to a template you registered.
OTP and verification codes
A short-lived code the user just asked for. Send it at once, and resend only when the user asks again, never from a retry that could fire twice.
Order updates
Confirmed, shipped, out for delivery. Queue each one once the order is stored, and keep one record per event so a retry cannot send it twice.
Payment updates
Received, refunded, invoiced. One message per payment event, keyed on the payment’s own ID.
Reminders
Appointments, delivery slots, due dates. These can wait in the queue, so check that the booking and the time are still current just before you send.
Promotional campaigns
A campaign needs a promotional template. TRAI’s default window for promotional SMS is 10:00 to 21:00, so a campaign queued at night waits for the morning.
Links in the text
A URL, APK, OTT link or call-back number that is not whitelisted has not been permitted in SMS since 1 October 2024. Whitelist a link on your DLT portal before it goes into a template.
Why does the send sit behind a queue?
So a traffic spike or a failed call never blocks your app or loses a message. The queue, the retry policy and the failed-jobs set are yours to build; the API call inside the worker is a single request.
Store the event, then queue it
Write the event and its status to your database first. If a worker dies, the row shows what still has to be sent.
One event, one SMS
Key each job on your own event ID, so a duplicate event cannot queue a second message.
Retry transient errors, then stop
Back off between attempts and stop at a limit you choose. The example uses three attempts with exponential backoff; those are the example’s numbers, not a recommendation from us.
Check before you resend
No idempotency key is documented for the send call, so a retry after a timeout can send twice. Look up your record of the send before you try again.
Know your plan’s rate limit
Keep the worker’s concurrency under it. The Rate Limiting & Throttling guide in our documentation library has the limits; we send it on request.
Keep a failed-jobs set
Messages that run out of retries go somewhere a person can see, with the error attached.
How do delivery reports come back?
By a webhook you control, not by polling. Ours sends message.delivered and message.failed, signed with X-NotifyTech-Signature. SMS has no read receipt, so Delivered is the last status you will see.
Events by webhook, not by polling
The developer page documents two events, message.delivered and message.failed. It documents no endpoint for polling a message’s status, so store what the webhook tells you. Give us your webhook URL when you get your keys.
Verify the signature, then reply 200
Each webhook carries an X-NotifyTech-Signature header. The sample checks it against your webhook secret, answers 401 if it does not match, and replies 200 once the event is handled.
Keep the status per message
Update one row per message from each event, and keep the error from message.failed beside it.
No report means unknown
If nothing arrives for a message, call it unknown, not failed and not delivered. A Delivered status is also not proof that a person read the message.
Is a second provider a code swap?
No. A fallback provider is a registration question before it is a code question. Your header, template and telemarketer chain have to match what you declared, so a second provider has to be registered before it can carry traffic.
A header that is registered
Commercial SMS may only use registered headers. A new provider means a header registered through that provider before it can send as you.
A template that matches
Each message is checked against a registered template, and a template belongs to one header. Moving traffic means registering the template again, not copying the text.
A declared telemarketer chain
Since December 2024, a message whose chain is not defined, or does not match the declared one, is rejected. Changing provider is also a change to your registration.
In what order do you build it?
Schema first, queue second, API call third. Each step gives the next one something to record against, so the order matters more than the speed.
Step 1: Design your event schema
List every user action that should send a message, the template each one maps to and the payload the worker needs.
Step 2: Set up the queue
Pick the queue, then set its concurrency, retry policy and failed-jobs set before you write any SMS code.
Step 3: Call the send endpoint
Add the API client, map each event to your registered DLT template and call the endpoint from the worker.
Step 4: Implement retries
Add backoff and failed-job handling, and test the failure cases against a mock endpoint of your own.
Step 5: Wire up the delivery webhook
Give us your webhook URL when you get your keys, verify the signature on each request and store the status.
Step 6: Add monitoring
Track failures and sent messages with no report in your own monitoring, and log each failure with its error.
What does the whole path look like?
One message, from the event in your app to the status in your table. Three stages are yours and two are documented on our side. Pick a stage to see its code or its rules.
You build and run this Documented on the developer page
Yours
Something happens in your app
An order is placed, a payment clears, a user asks for a code. Nothing here touches our API: your code decides that an event needs a message and which registered template it uses.
- Store the event before you queue it
- Key it on your own ID so one event sends one SMS
- Map it to a template you have registered
What to record per event
- Event ID
- Your own key. A duplicate event finds the existing row and stops.
- Recipient and template
- The number and the registered template this event uses.
- Status
- Starts as queued.
- Time
- When it was queued, so a reminder can check it is still current.
An example record, not a NotifyTech AI schema.
Yours
A queue between your app and the API
The queue absorbs a spike and keeps a failed call off the request path. Retries, concurrency and the failed-jobs set live here, and all of them are your policy.
- Retry transient errors with backoff, then stop
- Record each send before you retry it
- Keep concurrency under your plan’s rate limit
const Queue = require('bull');const axios = require('axios');const smsQueue = new Queue('sms', { redis: { host: 'localhost' } });// Your event handler: queue the messageapp.post('/webhooks/order-placed', async (req, res) => { const { phone, orderId } = req.body; await smsQueue.add({ to: phone, // example link only; links in SMS must be whitelisted on your DLT portal message: `Order ${orderId} confirmed. Track: https://example.com/t/${orderId}`, }, { attempts: 3, backoff: { type: 'exponential', delay: 1000 } }); res.json({ queued: true });});// Queue worker: one call to the SMS endpoint per job.// A retry after a timeout can send twice, so record each send first.smsQueue.process(async (job) => { await axios.post('https://api.notifytechai.com/v1/sms/send', { to: job.data.to, message: job.data.message, sender_id: 'YOUR_SENDER_ID', dlt_entity_id: 'YOUR_ENTITY_ID', dlt_template_id: 'YOUR_TEMPLATE_ID', }, { headers: { Authorization: 'Bearer YOUR_API_KEY' }, });});An example, not NotifyTech AI code. The three attempts and the backoff are the example’s numbers. The link is example.com; a real link has to be whitelisted on your DLT portal.
Ours, documented
One call to the send endpoint
The worker makes one request per message, authenticated with your API key as a Bearer token. The request carries your sender ID and the entity and template IDs from your own DLT registration.
- sender_id, dlt_entity_id and dlt_template_id travel with the text
- The text has to match the registered template
- No idempotency key is documented
import requestsdef send_sms(): url = "https://api.notifytechai.com/v1/sms/send" headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } payload = { "to": "+919876543210", "message": "Your OTP is 123456", "sender_id": "YOUR_SENDER_ID", "dlt_entity_id": "1234567890123456", "dlt_template_id": "1234567890123456789" } response = requests.post( url, json=payload, headers=headers ) return response.json()result = send_sms()print(result)The sample from the developer page. The key and the IDs are placeholders.
Ours, documented
The report comes back by webhook
Delivery reports arrive at a URL you give us. The documented events are message.delivered and message.failed, each signed with an X-NotifyTech-Signature header. Reply 200 once you have stored the event.
- Check the signature before you trust the body
- message.failed carries an error: keep it
- SMS has no read receipt: Delivered is the last status
<?php// webhook.php - Handle delivery reports$payload = file_get_contents('php://input');$data = json_decode($payload, true);$signature = $_SERVER['HTTP_X_NOTIFYTECH_SIGNATURE'];$secret = 'your_webhook_secret';if (!verifySignature($payload, $signature, $secret)) { http_response_code(401); exit('Invalid signature');}switch ($data['event']) { case 'message.delivered': updateMessageStatus( $data['message_id'], 'delivered' ); break; case 'message.failed': updateMessageStatus( $data['message_id'], 'failed' ); logError($data['error']); break;}http_response_code(200);echo json_encode(['status' => 'processed']);The sample from the developer page. The secret and the helper functions are placeholders.
Yours
The status you store is the truth your app uses
The webhook is the only report you get, so your table has to hold what it said and also what it did not say. A message with no report is unknown, and that is a state of its own.
- Update the row from the webhook, never from a guess
- Log every failure with its error
- Watch for sent messages that never report
Five states a row can be in
- queued
- Stored, waiting for a worker.
- submitted
- Your worker made the call.
- delivered
- message.delivered arrived. The last status SMS has.
- failed
- message.failed arrived, with its error.
- unknown
- Submitted, and no report yet. Not failed, not delivered: decide what your app does next.
An example set of states, not a NotifyTech AI schema.
Frequently Asked Questions
Common questions about building an SMS notification system
References & Further Reading
TRAI’s own documents behind the registration rules in this guide
- Read the Regulations
TCCCPR 2018, consolidated
TRAI’s Telecom Commercial Communications Customer Preference Regulations as consolidated on 21 May 2026: the rules for senders, headers, templates, consent and scrubbing. The gazetted text prevails.
- Read the Direction
TRAI Direction, 28 October 2024
Whitelisting of URLs, APKs, OTT links and call-back numbers, and rejection of messages whose telemarketer chain is undefined or does not match.
- Read the Release
SMS traceability, Press Release 92/2024
TRAI’s December 2024 note on the chain declaration and the rejection of messages sent through unregistered paths.
Request your API keys
Tell us what you are building and we will set up your API keys. Real people on support, Mon–Sat, 9–7 IST.