top of page

Bulk SMS API in Bangalore — A Developer's Complete Integration Reference

If you've landed on this page, you're probably evaluating SMS API providers for a specific integration: OTP for your login flow, transactional alerts for your app, or bulk notifications for your platform. You don't need another vendor's marketing page telling you they have "the best SMS API in India." You need to know: what does the request look like, how fast does it deliver, how do I handle DLT compliance without it becoming a nightmare, and what happens when something goes wrong at 2 AM.

This page answers all of that. It's written by engineers, for engineers — with actual code samples, real API response structures, error handling patterns, and an honest explanation of how TRAI DLT compliance works at the code level.

TechTo Networks provides a production-ready SMS API trusted by development teams at Bangalore startups and enterprise tech companies. Here is the full technical reference, plus everything you need to make an informed vendor decision.

Neon icon with "SMS" text centered. Background shows outlines of Bangalore landmarks. Text reads "BULK SMS IN BANGALORE, TECHTO NETWORKS."

Before You Write a Line of Code — Understanding the Indian SMS Landscape for Developers

If you're building in India for the first time (or coming from international SMS experience), the regulatory context is different enough that it's worth understanding before you start integrating.

TRAI DLT: The Compliance Layer That Cannot Be Ignored

TRAI's Distributed Ledger Technology (DLT) mandate, enforced from 2021 onwards, requires that every commercial SMS sender in India register their entity, sender IDs, and message templates before messages can be delivered. There is no workaround. No "test mode with real numbers." No grace period.

In practical terms for developers, this means:

  1. Every message sent to Indian mobile numbers must match a pre-approved template stored on TRAI's DLT registry

  2. The template match is evaluated at the carrier level — not your gateway — which means no code-side workaround can substitute for a registered template

  3. Variables in templates are defined using {#var#} notation in the TRAI system, but your SMS gateway (TechTo) translates this to developer-friendly {variable_name} syntax

  4. Sender IDs (the 6-character name appearing as "From") must be pre-registered and take 24–48 hours to activate

  5. During development and testing, you use our sandbox environment which bypasses DLT checks and sends to a simulated carrier — so you can build and test your integration completely before getting DLT approvals

Understanding this upfront saves you the classic mistake: building your full integration, setting a launch date, and then discovering your templates need 48 hours for approval.

Message Categories — Which Route Do You Need?

Category

Route Type

Delivery Target

DND Numbers

Delivery Window

OTP / One-Time Passwords

OTP Priority

<1 second

Delivered

24×7

Transactional (alerts, notifications, receipts)

Transactional

<3 seconds

Delivered

24×7

Promotional (marketing, offers, campaigns)

Promotional

<10 seconds

Blocked

9 AM – 9 PM only

Service Explicit (account-related)

Service

<5 seconds

Delivered

24×7

Rule of thumb: if the message is triggered by a user action or is essential for service delivery, use transactional or OTP routes. If it's marketing, use promotional. Using the wrong route is a DLT violation.

API Authentication and Base Setup

Base URL

All endpoints use HTTPS. HTTP connections are rejected at the load balancer.

Authentication

TechTo uses API key authentication via the Authorization header.

Authorization: Bearer YOUR_API_KEY

Your API key is generated from your TechTo dashboard under Settings → API Keys. You can create multiple keys with different permission scopes (send-only, read-only, admin) — useful for restricting what each microservice or team member's integration can do.

Best practices for API key security:

  • Store your key in environment variables, never hardcoded in source files

  • Use a separate key per environment (development, staging, production)

  • Rotate keys quarterly or immediately upon any suspected exposure

  • Use scope-limited keys for services that only need to send (not read delivery reports)

# Set in your environment
export TECHTO_API_KEY="your_key_here"

Core API Endpoints

1. Send a Single SMS

Endpoint: POST /v2/sms/send

Request:

POST https://api.techtonetworks.com/v2/sms/send
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "to": "919876543210",
  "message": "Your OTP for TechApp is 847291. Valid for 10 minutes. Do not share with anyone.",
  "sender_id": "TECHAP",
  "route": "otp",
  "template_id": "1007161234567890123",
  "unicode": false
}

Parameters:

Parameter

Type

Required

Description

to

string

Yes

Mobile number with country code. Format: 91XXXXXXXXXX. Do not include +.

message

string

Yes

Message content. Max 160 chars for single SMS, 306 for concatenated (2-part).

sender_id

string

Yes

6-character DLT-registered sender ID.

route

string

Yes

otp, transactional, promotional, or service

template_id

string

Yes (live)

Your DLT-registered template ID. Not required in sandbox mode.

unicode

boolean

No

Set true for Indian language (non-ASCII) messages. Doubles credit usage.

flash

boolean

No

Set true to send as a flash SMS (displayed immediately, not stored).

callback_url

string

No

Per-request webhook URL for delivery receipt. Overrides account default.

Success Response (HTTP 200):

{
  "status": "success",
  "request_id": "req_01HXZ4K8M2V3N9P",
  "messages": [
    {
      "message_id": "msg_01HXZ4K9A7B2C4D",
      "to": "919876543210",
      "status": "submitted",
      "credits_used": 1,
      "operator": "Jio",
      "submitted_at": "2026-05-29T10:23:45.312Z"
    }
  ]
}

Error Response (HTTP 4xx/5xx):

{
  "status": "error",
  "error_code": "TEMPLATE_MISMATCH",
  "error_message": "Message content does not match registered template ID 1007161234567890123",
  "request_id": "req_01HXZ4K8M2V3N9P"
}

2. Send Bulk SMS — Multiple Recipients

Endpoint: POST /v2/sms/bulk

For sending different personalized messages to multiple recipients in a single API call:

POST https://api.techtonetworks.com/v2/sms/bulk
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "sender_id": "MYAPP",
  "route": "transactional",
  "template_id": "1007169876543210987",
  "messages": [
    {
      "to": "919876543210",
      "variables": {
        "customer_name": "Rajan",
        "order_id": "ORD-10234",
        "amount": "₹1,499",
        "tracking_url": "https://trk.myapp.in/a9k2m"
      }
    },
    {
      "to": "919123456789",
      "variables": {
        "customer_name": "Priya",
        "order_id": "ORD-10235",
        "amount": "₹849",
        "tracking_url": "https://trk.myapp.in/b7j3n"
      }
    }
  ]
}

The template registered on DLT for this example would be:

Dear {customer_name}, your order {order_id} worth {amount} has shipped. Track: {tracking_url}

Limits:

  • Maximum 10,000 recipients per bulk request

  • For >10,000 recipients, use the async batch endpoint (/v2/sms/batch) with a callback URL

  • Rate limit: 1,000 API requests per minute per account (configurable upward for enterprise)

Bulk Response:

{
  "status": "success",
  "batch_id": "batch_01HXZ5M2N8P3Q7R",
  "total_submitted": 2,
  "total_credits": 2,
  "messages": [
    {
      "message_id": "msg_01HXZ5M3A1B2C3D",
      "to": "919876543210",
      "status": "submitted"
    },
    {
      "message_id": "msg_01HXZ5M4E5F6G7H",
      "to": "919123456789",
      "status": "submitted"
    }
  ]
}

3. Async Batch for High-Volume Sends (>10,000 Recipients)

For large campaigns — 100,000 recipients or more — synchronous API calls are the wrong pattern. Use the async batch endpoint:

POST https://api.techtonetworks.com/v2/sms/batch
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "sender_id": "SHOPNM",
  "route": "promotional",
  "template_id": "1007172345678901234",
  "scheduled_at": "2026-06-01T09:00:00+05:30",
  "callback_url": "https://yourapp.com/webhooks/techto/batch-complete",
  "recipients_url": "https://yourapp.com/api/campaign-recipients/camp_456"
}

The recipients_url should return a JSON array of recipient objects when called by TechTo's batch processor. This avoids the need to upload millions of phone numbers in a single request body.

The batch endpoint returns immediately with a batch_id. Processing happens asynchronously. Your callback_url receives a notification when the batch is complete.

4. Delivery Status Polling

Endpoint: GET /v2/sms/status/{message_id}

GET https://api.techtonetworks.com/v2/sms/status/msg_01HXZ4K9A7B2C4D
Authorization: Bearer YOUR_API_KEY

Response:

{
  "message_id": "msg_01HXZ4K9A7B2C4D",
  "to": "919876543210",
  "status": "delivered",
  "submitted_at": "2026-05-29T10:23:45.312Z",
  "delivered_at": "2026-05-29T10:23:45.891Z",
  "delivery_latency_ms": 579,
  "operator": "Jio",
  "operator_message_id": "JIO_20260529_A7X9K2",
  "credits_used": 1
}

Status values:

Status

Meaning

submitted

Accepted by TechTo, queued for carrier submission

dispatched

Submitted to carrier network

delivered

Carrier confirmed delivery to handset

failed

Delivery failed — check failure_reason

dnd_filtered

Number is DND-registered and route was promotional

invalid_number

Number does not exist or is not in service

template_rejected

Carrier DLT scrubber rejected the template match

Note on polling: For production systems handling OTPs or time-sensitive alerts, webhook delivery receipts (below) are significantly better than polling. Polling adds unnecessary API calls and latency to your status pipeline.

5. OTP-Specific Endpoint

The OTP endpoint is a higher-level abstraction over the standard send endpoint, designed for common OTP patterns:

POST https://api.techtonetworks.com/v2/otp/send
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "to": "919876543210",
  "otp_length": 6,
  "expiry_seconds": 600,
  "template_id": "1007175678901234567",
  "app_name": "MyApp",
  "purpose": "login"
}

TechTo generates the OTP value server-side and injects it into your DLT-approved template. The response includes a signed otp_session_token you use to verify OTP submission from the user — no need to store raw OTP values in your database.

Response:

{
  "status": "success",
  "message_id": "msg_01HXZ7P2Q3R4S5T",
  "otp_session_token": "sess_eyJhbGciOiJIUzI1NiJ9.eyJ0byI6IjkxOTg3NjU0MzIxMCIsImV4cCI6MTc0ODUxMjIyNX0.xyz",
  "expires_at": "2026-05-29T10:33:45Z"
}

Verify OTP submission:

POST https://api.techtonetworks.com/v2/otp/verify
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "otp_session_token": "sess_eyJhbGciOiJIUzI1NiJ9...",
  "otp_entered": "847291"
}

Verify Response:

{
  "valid": true,
  "verified_at": "2026-05-29T10:25:12Z",
  "attempts_remaining": null
}

Invalid OTP response includes attempts_remaining (we enforce a maximum of 3 incorrect attempts per session before the token is invalidated) and valid: false.

Webhook Configuration — Real-Time Delivery Receipts

Webhooks are the right pattern for production SMS systems. Don't poll — let TechTo push delivery events to you.

Configuring Your Webhook Endpoint

In your TechTo dashboard: Settings → Webhooks → Add Endpoint

Or via API:

POST https://api.techtonetworks.com/v2/webhooks
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "url": "https://yourapp.com/webhooks/sms-delivery",
  "events": ["message.delivered", "message.failed", "message.dnd_filtered"],
  "secret": "your_webhook_signing_secret"
}

Delivery Receipt Payload

{
  "event": "message.delivered",
  "timestamp": "2026-05-29T10:23:45.918Z",
  "data": {
    "message_id": "msg_01HXZ4K9A7B2C4D",
    "to": "919876543210",
    "status": "delivered",
    "operator": "Airtel",
    "delivered_at": "2026-05-29T10:23:45.891Z",
    "delivery_latency_ms": 579,
    "sender_id": "TECHAP",
    "route": "otp"
  },
  "signature": "sha256=a4b8c2d1e9f3..."
}

Verifying Webhook Signatures

Every webhook payload is signed using HMAC-SHA256 with your webhook secret. Always verify this before processing:

Node.js:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// In your Express handler:
app.post('/webhooks/sms-delivery', express.json(), (req, res) => {
  const signature = req.headers['x-techto-signature'];
  
  if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the event
  const { event, data } = req.body;
  
  if (event === 'message.delivered') {
    // Update your order/notification status in DB
    await markNotificationDelivered(data.message_id);
  }
  
  res.status(200).send('OK');
});

Python (Flask):

import hmac
import hashlib
import json
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')

def verify_signature(payload, signature):
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        json.dumps(payload, separators=(',', ':')).encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhooks/sms-delivery', methods=['POST'])
def sms_delivery_webhook():
    signature = request.headers.get('X-TechTo-Signature')
    payload = request.get_json()
    
    if not verify_signature(payload, signature):
        return jsonify({'error': 'Invalid signature'}), 401
    
    event = payload['event']
    data = payload['data']
    
    if event == 'message.delivered':
        update_notification_status(data['message_id'], 'delivered')
    elif event == 'message.failed':
        handle_delivery_failure(data['message_id'], data.get('failure_reason'))
    
    return '', 200

Webhook Retry Policy

If your endpoint returns anything other than HTTP 200–299, TechTo retries the webhook with exponential backoff:

  • Retry 1: 30 seconds after initial failure

  • Retry 2: 5 minutes

  • Retry 3: 30 minutes

  • Retry 4: 2 hours

  • Retry 5: 12 hours (final attempt)

Failed webhook events remain visible in your dashboard's Webhook Logs for up to 7 days and can be manually replayed.

DLT Template Management via API

Register a Template Programmatically

POST https://api.techtonetworks.com/v2/dlt/templates
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "template_name": "order_shipped_notification",
  "category": "transactional",
  "sender_id": "MYAPP",
  "content": "Dear {customer_name}, your order {order_id} has shipped. Track your delivery at {tracking_url}. Expected by {delivery_date}.",
  "variables": ["customer_name", "order_id", "tracking_url", "delivery_date"]
}

Response:

{
  "template_id": "1007178901234567890",
  "status": "pending_approval",
  "submitted_at": "2026-05-29T10:30:00Z",
  "estimated_approval": "2026-05-30T10:30:00Z"
}

Check Template Approval Status

GET https://api.techtonetworks.com/v2/dlt/templates/1007178901234567890
Authorization: Bearer YOUR_API_KEY

List All Registered Templates

GET https://api.techtonetworks.com/v2/dlt/templates?status=approved&page=1&limit=50
Authorization: Bearer YOUR_API_KEY

Template Best Practices for Developers

Variable naming: Use descriptive, consistent variable names. {customer_name} is better than {var1}. This matters because TRAI's DLT system and your TechTo template both need to agree on variable positions.

Template versioning: When you need to change an approved template, you must submit a new template — not edit the existing one. Plan for 48-hour approval windows when making template changes. Keep your template_id values in a config file or environment variable, not hardcoded in business logic, so they're easy to update.

Template pre-approval in staging: Run your DLT template submissions from your staging environment before your production launch date. The 48-hour approval window should be factored into your deployment timeline.

Fallback templates: For critical transactional messages (payment OTPs, fraud alerts), maintain an approved fallback template that's simpler and less variable-heavy. If your primary template gets flagged in a DLT scrubbing update, your fallback keeps working while the primary is re-approved.

Language and Unicode SMS

Sending Kannada, Hindi, and Other Indian Language SMS

POST https://api.techtonetworks.com/v2/sms/send
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

{
  "to": "919876543210",
  "message": "ನಿಮ್ಮ ಆರ್ಡರ್ ORD-10234 ರವಾನೆಯಾಗಿದೆ. ಟ್ರ್ಯಾಕ್ ಮಾಡಲು: https://trk.app.in/k9m2",
  "sender_id": "MYAPP",
  "route": "transactional",
  "template_id": "1007180123456789012",
  "unicode": true
}

Credit counting for Unicode:

  • Standard SMS (ASCII): 160 characters = 1 credit

  • Unicode SMS (Indian language scripts): 70 characters = 1 credit

  • Concatenated Unicode: 67 characters per part = additional credits

Always calculate unicode message credits before sending large batches. A 130-character Kannada message costs 2 credits, not 1.

SDK Reference

Node.js SDK

npm install techto-sms
const TechTo = require('techto-sms');

const client = new TechTo({
  apiKey: process.env.TECHTO_API_KEY,
  sandbox: process.env.NODE_ENV !== 'production'
});

// Send OTP
async function sendLoginOTP(phoneNumber) {
  try {
    const result = await client.otp.send({
      to: phoneNumber,
      templateId: process.env.OTP_TEMPLATE_ID,
      appName: 'MyApp',
      purpose: 'login',
      expirySeconds: 600
    });
    
    return {
      sessionToken: result.otpSessionToken,
      expiresAt: result.expiresAt
    };
  } catch (error) {
    if (error.code === 'INVALID_NUMBER') {
      throw new Error('Phone number is not valid');
    }
    throw error; // Re-throw unexpected errors
  }
}

// Verify OTP
async function verifyLoginOTP(sessionToken, userEnteredOTP) {
  const result = await client.otp.verify({
    sessionToken,
    otp: userEnteredOTP
  });
  
  return result.valid;
}

// Send transactional notification
async function sendOrderUpdate(phoneNumber, orderData) {
  await client.sms.send({
    to: phoneNumber,
    templateId: process.env.ORDER_UPDATE_TEMPLATE_ID,
    senderId: 'MYAPP',
    route: 'transactional',
    variables: {
      customer_name: orderData.customerName,
      order_id: orderData.orderId,
      status: orderData.status,
      tracking_url: orderData.trackingUrl
    }
  });
}

Python SDK

pip install techto-sms
from techto import TechToClient, SMSRoute
import os

client = TechToClient(
    api_key=os.environ['TECHTO_API_KEY'],
    sandbox=os.environ.get('ENVIRONMENT') != 'production'
)

def send_payment_alert(phone: str, amount: str, merchant: str, txn_id: str):
    """Send transactional payment SMS."""
    try:
        result = client.sms.send(
            to=phone,
            template_id=os.environ['PAYMENT_ALERT_TEMPLATE_ID'],
            sender_id='MYBANK',
            route=SMSRoute.TRANSACTIONAL,
            variables={
                'amount': amount,
                'merchant': merchant,
                'txn_id': txn_id
            }
        )
        return result.message_id
    except client.errors.TemplateRejected:
        # Log and alert — template may need re-approval
        logger.error(f"Template rejected for payment alert to {phone}")
        raise
    except client.errors.InvalidNumber:
        logger.warning(f"Invalid phone number: {phone}")
        return None

def send_bulk_campaign(recipients: list, template_id: str):
    """Send bulk promotional campaign."""
    batch = client.sms.bulk(
        sender_id='MYSHOP',
        route=SMSRoute.PROMOTIONAL,
        template_id=template_id,
        messages=[
            {
                'to': r['phone'],
                'variables': r['variables']
            }
            for r in recipients
        ]
    )
    return batch.batch_id

PHP SDK

composer require techto/sms
<?php
use TechTo\SMS\Client;
use TechTo\SMS\Routes;

$client = new Client([
    'api_key' => $_ENV['TECHTO_API_KEY'],
    'sandbox' => $_ENV['APP_ENV'] !== 'production'
]);

// Send OTP
function sendOTP(string $phone): array {
    global $client;
    
    $result = $client->otp()->send([
        'to' => $phone,
        'template_id' => $_ENV['OTP_TEMPLATE_ID'],
        'app_name' => 'MyApp',
        'expiry_seconds' => 300
    ]);
    
    return [
        'session_token' => $result->otpSessionToken,
        'expires_at' => $result->expiresAt
    ];
}

Error Handling Reference

All API errors return structured JSON with an error_code field. Build your error handling around these codes:

Error Code

HTTP Status

Meaning

Recommended Action

INVALID_API_KEY

401

API key missing, malformed, or revoked

Check environment variable; rotate key if compromised

INSUFFICIENT_CREDITS

402

Account credit balance too low

Top up credits or set up auto-recharge

INVALID_NUMBER

422

Number format invalid or not in service

Validate number before calling API

TEMPLATE_MISMATCH

422

Message content doesn't match approved template

Fix message content or submit new template

TEMPLATE_NOT_FOUND

422

Template ID doesn't exist in your account

Check template_id value

SENDER_NOT_REGISTERED

422

Sender ID not DLT-registered

Register sender ID (24–48hr process)

ROUTE_RESTRICTED

422

Trying transactional template on promotional route or vice versa

Check route parameter

RATE_LIMIT_EXCEEDED

429

Too many requests per minute

Implement exponential backoff

CARRIER_REJECTED

503

Carrier temporarily rejecting submissions

Retry with backoff; check status page

INTERNAL_ERROR

500

TechTo internal error

Retry once; if persistent, contact support

Idempotency Keys — Preventing Duplicate Messages

For critical transactional messages (OTPs, payment alerts), use idempotency keys to prevent duplicate sends in case of network timeouts:

POST https://api.techtonetworks.com/v2/sms/send
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
Idempotency-Key: order_10234_shipped_notification_v1

{
  "to": "919876543210",
  ...
}

If you retry a request with the same Idempotency-Key within 24 hours, TechTo returns the original response without sending a duplicate message. This is essential for retry logic in payment-critical notification flows.

Integration Patterns for Common Bangalore Tech Stacks

Django + Celery (Python)

# tasks.py
from celery import shared_task
from techto import TechToClient
import logging

logger = logging.getLogger(__name__)
client = TechToClient(api_key=settings.TECHTO_API_KEY)

@shared_task(
    bind=True,
    max_retries=3,
    default_retry_delay=30,
    autoretry_for=(Exception,),
    retry_backoff=True
)
def send_order_confirmation_sms(self, order_id: int):
    """Send order confirmation SMS with Celery retry logic."""
    from orders.models import Order
    
    order = Order.objects.select_related('customer').get(id=order_id)
    
    try:
        result = client.sms.send(
            to=order.customer.phone,
            template_id=settings.ORDER_CONFIRMATION_TEMPLATE_ID,
            sender_id='MYSHOP',
            route='transactional',
            variables={
                'customer_name': order.customer.first_name,
                'order_id': str(order.id),
                'amount': str(order.total),
                'delivery_date': order.estimated_delivery.strftime('%d %b')
            }
        )
        
        # Store message_id for delivery tracking
        order.sms_message_id = result.message_id
        order.save(update_fields=['sms_message_id'])
        
        logger.info(f"Order {order_id} confirmation SMS sent: {result.message_id}")
        
    except Exception as exc:
        logger.error(f"SMS send failed for order {order_id}: {exc}")
        raise self.retry(exc=exc)

Express.js + Bull Queue (Node.js)

// smsQueue.js
const Bull = require('bull');
const TechTo = require('techto-sms');

const smsQueue = new Bull('sms-notifications', process.env.REDIS_URL);
const techto = new TechTo({ apiKey: process.env.TECHTO_API_KEY });

// Process queue
smsQueue.process('send-notification', async (job) => {
  const { to, templateId, senderId, route, variables } = job.data;
  
  return await techto.sms.send({ to, templateId, senderId, route, variables });
});

// Add to queue from your route handlers
async function queueOrderSMS(order) {
  await smsQueue.add('send-notification', {
    to: order.customerPhone,
    templateId: process.env.ORDER_SHIPPED_TEMPLATE,
    senderId: 'MYAPP',
    route: 'transactional',
    variables: {
      customer_name: order.customerName,
      order_id: order.id,
      tracking_url: `https://trk.myapp.in/${order.trackingCode}`
    }
  }, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 },
    removeOnComplete: 100,
    removeOnFail: 500
  });
}

Sandbox Environment

Every TechTo account has access to a full sandbox environment for development and testing:

Sandbox behaviour:

  • DLT template validation is skipped — use any template_id format

  • Messages are not submitted to real carriers — no actual SMS is sent

  • Delivery receipts are simulated based on the sandbox_simulate_status parameter you can pass

  • Credit balance is not deducted

  • All API responses match production format exactly — no surprises when you go live

Simulating different delivery statuses:

{
  "to": "919876543210",
  "message": "Test message",
  "sender_id": "MYAPP",
  "route": "transactional",
  "sandbox_simulate_status": "failed",
  "sandbox_simulate_failure_reason": "CARRIER_REJECTED"
}

This lets you test your error handling paths — what your app does when an SMS fails — without needing to manipulate real carrier behavior.

API Rate Limits and Throughput

Account Type

Requests/Min

Messages/Sec (burst)

Concurrent Connections

Developer (Free)

60

50

2

Startup

300

500

10

Growth

1,000

2,000

25

Enterprise

Custom

Up to 50,000

Unlimited

Rate limit headers are returned on every response:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1748521500

When you exceed the rate limit, the API returns HTTP 429 with a Retry-After header. Implement exponential backoff with jitter in all production retry logic.

Frequently Asked Questions for Developers

Q: Can I test the API without completing DLT registration? Yes, completely. The sandbox environment requires zero DLT registration. You can build, integrate, and test your entire SMS flow in sandbox mode. Only when you're ready for production messages to real Indian numbers do you need DLT registration in place.

Q: How long does DLT template approval actually take? In our experience, straightforward transactional templates are approved within 24–36 hours on weekdays. Complex templates with many variables, or templates submitted during TRAI system maintenance windows, can take up to 72 hours. Plan your launch timelines accordingly and submit templates at least a week before your go-live date.

Q: My OTP is arriving in 3–5 seconds, not under 1 second. What's happening? A few possible causes: (1) You're not using the otp route — the promotional route is much slower. (2) Your template is mismatched and the carrier is triggering secondary DLT validation. (3) The carrier network is experiencing congestion — check our status page. (4) You're using an aggregator-connected sandbox endpoint — switch to production. If the issue persists on production OTP route, contact support with specific message IDs.

Q: What's the maximum message length I should use? 160 characters for a single-credit ASCII SMS. At 161 characters, the message splits into 2 parts (153 chars each) and costs 2 credits. If you're routinely near the limit, redesign your template to stay under 160 chars — short URLs help significantly (our short URL service generates 20-character links).

Q: How should I handle the phone number format in the API? Always use 91XXXXXXXXXX format — country code followed by 10-digit mobile number, no +, no spaces, no hyphens. Validate this server-side before calling the API. Numbers not starting with 6, 7, 8, or 9 (after the country code) are not valid Indian mobile numbers.

Q: Can I use TechTo's API for sending international SMS outside India? TechTo's current production routes cover India only. If your application needs international SMS capability alongside Indian, contact us — we can discuss routing options. Don't attempt to send international numbers through Indian DLT routes; it will fail at the carrier level.

Q: Is there a webhook for inbound SMS (customer replies)? Yes. If you have a virtual long code or short code enabled on your account, inbound messages are pushed to your configured inbound webhook endpoint in real time. The payload includes the sender's number, message content, and timestamp. Contact support to enable virtual number assignment.

Q: Can I get real-time delivery data pushed to my analytics pipeline (Segment, Mixpanel, Amplitude)? Yes, through webhooks. Configure your delivery receipt webhook to post to your own analytics ingestion endpoint, then forward to your analytics platform. Alternatively, TechTo has native integrations with Segment and MoEngage — contact support for integration setup.

[CTA: Get Free API Key — Start in Sandbox →] [CTA: View Full API Documentation →] [CTA: Join Developer Slack Community →]

Internal Links: → Enterprise SMS Page | → OTP SMS Platform | → WhatsApp Business API | → Pricing Schema Markup: TechArticle + FAQPage + SoftwareApplication + HowTo Word Count Target: 5,300+ words

Advanced API Patterns for Production Systems

Implementing a Robust OTP Flow End-to-End

A complete, production-ready OTP implementation involves more than just calling the send API. Here's how to structure a full OTP authentication flow:

Backend flow (Node.js + Express example):

// routes/auth.js
const express = require('express');
const router = express.Router();
const TechTo = require('techto-sms');
const rateLimit = require('express-rate-limit');

const techto = new TechTo({ apiKey: process.env.TECHTO_API_KEY });

// Rate limit: 5 OTP requests per phone number per 15 minutes
const otpRequestLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  keyGenerator: (req) => req.body.phone,
  message: { error: 'Too many OTP requests. Please try again in 15 minutes.' }
});

// Request OTP
router.post('/request-otp', otpRequestLimiter, async (req, res) => {
  const { phone } = req.body;
  
  // Validate phone format
  if (!/^[6-9]\d{9}$/.test(phone)) {
    return res.status(400).json({ error: 'Invalid phone number format' });
  }
  
  try {
    const result = await techto.otp.send({
      to: `91${phone}`,
      templateId: process.env.LOGIN_OTP_TEMPLATE_ID,
      appName: 'YourApp',
      purpose: 'login',
      expirySeconds: 600
    });
    
    // Store session token server-side (Redis or your DB)
    await redis.setex(
      `otp_session:${phone}`,
      660, // 11 minutes (10 + 1 min buffer)
      result.otpSessionToken
    );
    
    res.json({
      success: true,
      message: 'OTP sent successfully',
      expiresIn: 600
    });
    
  } catch (error) {
    if (error.code === 'INVALID_NUMBER') {
      return res.status(400).json({ error: 'This number is not reachable' });
    }
    // Log error internally but don't expose details to client
    console.error('OTP send error:', error);
    res.status(500).json({ error: 'Failed to send OTP. Please try again.' });
  }
});

// Verify OTP
router.post('/verify-otp', async (req, res) => {
  const { phone, otp } = req.body;
  
  // Retrieve session token from Redis
  const sessionToken = await redis.get(`otp_session:${phone}`);
  
  if (!sessionToken) {
    return res.status(400).json({ error: 'OTP expired or not requested. Please request a new OTP.' });
  }
  
  try {
    const result = await techto.otp.verify({
      sessionToken,
      otp
    });
    
    if (result.valid) {
      // Delete session token after successful verification
      await redis.del(`otp_session:${phone}`);
      
      // Create your auth session / JWT here
      const authToken = generateAuthToken(phone);
      
      return res.json({ success: true, token: authToken });
    }
    
    // Invalid OTP
    res.status(400).json({
      error: 'Incorrect OTP',
      attemptsRemaining: result.attemptsRemaining
    });
    
  } catch (error) {
    console.error('OTP verify error:', error);
    res.status(500).json({ error: 'Verification failed. Please try again.' });
  }
});

This implementation covers the major production concerns: rate limiting on OTP requests (prevents OTP bombing attacks), server-side session token storage (prevents client-side manipulation), proper error handling without leaking internal details, and session cleanup after successful verification.

Building a Delivery Tracking System

For applications that need to display SMS delivery status to users (e-commerce order notifications, banking alerts), here's how to build a lightweight delivery tracking system:

// models/SmsDelivery.js (Mongoose schema)
const smsDeliverySchema = new mongoose.Schema({
  messageId: { type: String, required: true, unique: true, index: true },
  entityId: { type: String, required: true, index: true }, // e.g., orderId, transactionId
  entityType: { type: String, required: true }, // 'order', 'transaction', 'appointment'
  recipient: { type: String, required: true },
  status: {
    type: String,
    enum: ['submitted', 'dispatched', 'delivered', 'failed', 'dnd_filtered'],
    default: 'submitted'
  },
  submittedAt: { type: Date, default: Date.now },
  deliveredAt: { type: Date },
  deliveryLatencyMs: { type: Number },
  operator: { type: String },
  failureReason: { type: String }
});

// Webhook handler updates delivery records
app.post('/webhooks/sms-delivery', async (req, res) => {
  const { event, data } = req.body;
  
  if (['message.delivered', 'message.failed', 'message.dnd_filtered'].includes(event)) {
    await SmsDelivery.findOneAndUpdate(
      { messageId: data.message_id },
      {
        status: data.status,
        deliveredAt: data.delivered_at ? new Date(data.delivered_at) : undefined,
        deliveryLatencyMs: data.delivery_latency_ms,
        operator: data.operator,
        failureReason: data.failure_reason
      }
    );
  }
  
  res.status(200).send('OK');
});

Multi-Environment Configuration Pattern

Managing API keys and template IDs across development, staging, and production environments is a common pain point. Here's a clean configuration pattern:

// config/sms.js
const smsConfig = {
  development: {
    apiKey: process.env.TECHTO_DEV_API_KEY,
    sandbox: true,
    templates: {
      loginOtp: 'dev_template_001',
      orderShipped: 'dev_template_002',
      paymentAlert: 'dev_template_003'
    },
    senderIds: {
      transactional: 'MYAPP',
      promotional: 'MYSHOP'
    }
  },
  staging: {
    apiKey: process.env.TECHTO_STAGING_API_KEY,
    sandbox: true, // Use sandbox even in staging to avoid real SMS during QA
    templates: {
      loginOtp: process.env.OTP_TEMPLATE_ID_STAGING,
      orderShipped: process.env.ORDER_TEMPLATE_ID_STAGING,
      paymentAlert: process.env.PAYMENT_TEMPLATE_ID_STAGING
    },
    senderIds: {
      transactional: 'MYAPP',
      promotional: 'MYSHOP'
    }
  },
  production: {
    apiKey: process.env.TECHTO_API_KEY,
    sandbox: false,
    templates: {
      loginOtp: process.env.OTP_TEMPLATE_ID,
      orderShipped: process.env.ORDER_TEMPLATE_ID,
      paymentAlert: process.env.PAYMENT_TEMPLATE_ID
    },
    senderIds: {
      transactional: 'MYAPP',
      promotional: 'MYSHOP'
    }
  }
};

module.exports = smsConfig[process.env.NODE_ENV || 'development'];

Store template IDs in environment variables for all non-development environments. This makes template updates (when you submit a new DLT template and get a new template_id) a configuration change rather than a code deployment.

Monitoring and Observability for SMS in Production

Once your SMS integration is live, you need visibility into its health. Here's what to monitor:

Delivery rate by route and operator: Track the percentage of messages reaching "delivered" status, broken down by route (OTP/transactional/promotional) and operator (Jio/Airtel/Vi/BSNL). A sudden drop in delivery rate on a specific operator is an early warning of a carrier-side issue.

P95 and P99 delivery latency: For OTP flows, the 95th percentile delivery time is the number that matters — not the average. If your P95 OTP latency is 4 seconds, 5% of your users are waiting long enough to retry or abandon. Track this daily and alert on degradation.

Template rejection rate: Monitor the count of messages failing with TEMPLATE_MISMATCH or TEMPLATE_REJECTED error codes. A spike here indicates a DLT scrubbing event has affected your templates — act immediately to resubmit.

Credit balance alerts: Set up an alert when your credit balance drops below a threshold (e.g., 3 days of average daily usage). Running out of credits mid-campaign is entirely preventable.

Webhook delivery failures: Monitor your webhook processing logs for failures. Unprocessed delivery receipts mean your delivery status database is stale — which affects any user-facing delivery tracking UI.

TechTo's Reporting API exposes all of these metrics in machine-readable format, making integration with your monitoring stack (Datadog, Prometheus, Grafana, CloudWatch) straightforward.

Security Checklist for Production SMS Integration

Before going live, walk through this security checklist:

  • [ ] API key stored in environment variable, not hardcoded in source

  • [ ] Separate API keys for development, staging, production

  • [ ] API key scopes restricted to minimum required (send-only for microservices that only send)

  • [ ] Webhook signature verification implemented and tested

  • [ ] OTP rate limiting implemented (prevent OTP bombing attacks)

  • [ ] OTP session tokens stored server-side (not in cookies or localStorage)

  • [ ] Phone number validation before API calls (prevent invalid number errors at scale)

  • [ ] Idempotency keys used for critical transactional sends

  • [ ] Retry logic implements exponential backoff with jitter

  • [ ] Error handling does not leak internal API errors to end users

  • [ ] Delivery webhook endpoint handles duplicate deliveries idempotently

  • [ ] Credit balance monitoring alert configured

  • [ ] Template IDs in environment variables, not hardcoded

Completing this checklist before your launch saves the classic production incident where a hardcoded API key ends up in a public GitHub repository, or OTP bombing runs up your credit balance in minutes.

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page