387 lines
12 KiB
Markdown
387 lines
12 KiB
Markdown
# Ultronsms — generic DLT template SMS
|
|
|
|
Parameterized integration for sending **any DLT-approved SMS** via [Ultronsms](https://ultronsms.com) by supplying `peid`, `dlttemplateid`, sender ID, and filled template text at runtime.
|
|
|
|
Use this when you have multiple approved templates (OTP, password reset, alerts, etc.) and want one reusable client instead of hardcoding each flow.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
```
|
|
┌──────────────┐ peid + dlttemplateid ┌─────────────────┐ SendSMS ┌─────────────┐
|
|
│ Caller │ senderid + message │ Generic client │ ───────────► │ Ultronsms │
|
|
│ (your app) │ ───────────────────────► │ (this doc) │ │ gateway │
|
|
└──────────────┘ └─────────────────┘ └──────┬──────┘
|
|
│ │
|
|
│ GetDelivery (optional) │
|
|
◄──────────────────────────────┘
|
|
```
|
|
|
|
**Caller provides**
|
|
|
|
| Input | Description |
|
|
|-------|-------------|
|
|
| `mobile` | Recipient number (`+91…` or `91…`) |
|
|
| `sender_id` | DLT-approved header, e.g. `URMIAI`, `WEBDMG` |
|
|
| `pe_id` | Principal Entity ID linked to that sender |
|
|
| `dlt_template_id` | Approved content template ID |
|
|
| `message` | Final SMS text with all `{#var#}` / `{#num#}` placeholders replaced |
|
|
| `channel` | `Trans` (transactional) or `Promo` (promotional) — default `Trans` |
|
|
|
|
**Client returns**
|
|
|
|
- Send response with `JobId` and `MessageId`
|
|
- Optional delivery report via `GetDelivery`
|
|
|
|
---
|
|
|
|
## Environment configuration
|
|
|
|
Store credentials in environment variables (never commit secrets).
|
|
|
|
| Variable | Example | Description |
|
|
|----------|---------|-------------|
|
|
| `ULTRONSMS_BASE_URL` | `https://ultronsms.com` | Gateway base URL |
|
|
| `ULTRONSMS_USER` | `GENZENITH` | Account username |
|
|
| `ULTRONSMS_PASSWORD` | *(secret)* | Account password |
|
|
|
|
### Route mapping
|
|
|
|
| Channel | `channel` param | `route` param |
|
|
|---------|-----------------|---------------|
|
|
| Transactional | `Trans` | `2` |
|
|
| Promotional | `Promo` | `1` |
|
|
|
|
The UI may show internal route labels (e.g. `T_22`); the API expects numeric routes above.
|
|
|
|
---
|
|
|
|
## API endpoints
|
|
|
|
| Action | Method | URL |
|
|
|--------|--------|-----|
|
|
| Send SMS | `GET` | `{BASE_URL}/api/mt/SendSMS` |
|
|
| Delivery status | `GET` | `{BASE_URL}/api/mt/GetDelivery` |
|
|
|
|
### SendSMS — all parameters
|
|
|
|
| Parameter | Required | Notes |
|
|
|-----------|----------|-------|
|
|
| `user` | yes | From `ULTRONSMS_USER` |
|
|
| `password` | yes | From `ULTRONSMS_PASSWORD` |
|
|
| `senderid` | yes | DLT-approved header |
|
|
| `channel` | yes | `Trans` or `Promo` |
|
|
| `route` | yes | `2` (Trans) or `1` (Promo) |
|
|
| `number` | yes | Country code + number, no `+` |
|
|
| `text` | yes | Filled DLT template body |
|
|
| `peid` | yes | PE ID for this sender |
|
|
| `dlttemplateid` | yes | Lowercase parameter name |
|
|
| `DCS` | yes | `0` = English, `8` = Unicode |
|
|
| `flashsms` | yes | `0` = normal, `1` = flash |
|
|
|
|
### GetDelivery parameters
|
|
|
|
| Parameter | Required |
|
|
|-----------|----------|
|
|
| `user` | yes |
|
|
| `password` | yes |
|
|
| `jobid` | yes — from SendSMS `JobId` |
|
|
|
|
---
|
|
|
|
## DLT template rules
|
|
|
|
1. **Exact match** — Sent text must match the approved template except for variable substitutions.
|
|
2. **Replace all placeholders** — Common patterns: `{#var#}`, `{#num#}`. Fill every occurrence.
|
|
3. **Sender ↔ PE ID** — Each sender ID is bound to a PE ID in the DLT portal; they must match.
|
|
4. **Template ID** — Use the ID from the approved template row, not the PE ID.
|
|
|
|
### Example templates (verified in testing)
|
|
|
|
**URMI OTP** (transactional)
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Sender | `URMIAI` |
|
|
| PE ID | `1701178780498045011` |
|
|
| Template ID | `1777178797989033323` |
|
|
| Template | `Your URMI app verification code is {#num#}. It is valid for {#num#} minutes. Do not share it to anyone - URMI AI` |
|
|
| Example message | `Your URMI app verification code is 482916. It is valid for 10 minutes. Do not share it to anyone - URMI AI` |
|
|
|
|
**Password reset** (transactional)
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Sender | `WEBDMG` |
|
|
| PE ID | `1301159136621106678` |
|
|
| Template ID | `1307161537613263303` |
|
|
| Template | `Hello Customer, Your new password is '{#var#}'. Thank you. Regards, WebXion` |
|
|
| Example message | `Hello Customer, Your new password is 'Kx9#mP2q'. Thank you. Regards, WebXion` |
|
|
|
|
---
|
|
|
|
## Python implementation (generic)
|
|
|
|
```python
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Any
|
|
from urllib.parse import urlencode
|
|
from urllib.request import urlopen
|
|
import json
|
|
|
|
|
|
class SmsChannel(str, Enum):
|
|
TRANSACTIONAL = "Trans"
|
|
PROMOTIONAL = "Promo"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DltTemplate:
|
|
"""Register each approved template in your app or database."""
|
|
sender_id: str
|
|
pe_id: str
|
|
dlt_template_id: str
|
|
channel: SmsChannel = SmsChannel.TRANSACTIONAL
|
|
|
|
@property
|
|
def route(self) -> str:
|
|
return "2" if self.channel == SmsChannel.TRANSACTIONAL else "1"
|
|
|
|
|
|
@dataclass
|
|
class SendResult:
|
|
success: bool
|
|
job_id: str | None
|
|
message_id: str | None
|
|
number: str | None
|
|
raw: dict[str, Any]
|
|
|
|
|
|
class UltronsmsClient:
|
|
def __init__(
|
|
self,
|
|
base_url: str | None = None,
|
|
user: str | None = None,
|
|
password: str | None = None,
|
|
) -> None:
|
|
self.base_url = (base_url or os.environ["ULTRONSMS_BASE_URL"]).rstrip("/")
|
|
self.user = user or os.environ["ULTRONSMS_USER"]
|
|
self.password = password or os.environ["ULTRONSMS_PASSWORD"]
|
|
|
|
@staticmethod
|
|
def normalize_phone(number: str) -> str:
|
|
digits = "".join(c for c in number if c.isdigit())
|
|
if len(digits) == 10:
|
|
return f"91{digits}"
|
|
return digits
|
|
|
|
def _get(self, path: str, params: dict[str, str]) -> dict[str, Any]:
|
|
url = f"{self.base_url}{path}?{urlencode(params)}"
|
|
with urlopen(url, timeout=30) as resp:
|
|
return json.loads(resp.read().decode())
|
|
|
|
def send_template_sms(
|
|
self,
|
|
mobile: str,
|
|
message: str,
|
|
template: DltTemplate,
|
|
*,
|
|
dcs: str = "0",
|
|
flash_sms: str = "0",
|
|
) -> SendResult:
|
|
"""
|
|
Send SMS using any DLT-approved template.
|
|
|
|
Args:
|
|
mobile: Recipient, e.g. +916296346452
|
|
message: Fully substituted DLT template text
|
|
template: sender_id, pe_id, dlt_template_id, channel
|
|
"""
|
|
number = self.normalize_phone(mobile)
|
|
params = {
|
|
"user": self.user,
|
|
"password": self.password,
|
|
"senderid": template.sender_id,
|
|
"channel": template.channel.value,
|
|
"route": template.route,
|
|
"DCS": dcs,
|
|
"flashsms": flash_sms,
|
|
"number": number,
|
|
"text": message,
|
|
"peid": template.pe_id,
|
|
"dlttemplateid": template.dlt_template_id,
|
|
}
|
|
raw = self._get("/api/mt/SendSMS", params)
|
|
ok = raw.get("ErrorCode") == "000"
|
|
message_data = (raw.get("MessageData") or [{}])[0]
|
|
return SendResult(
|
|
success=ok,
|
|
job_id=raw.get("JobId"),
|
|
message_id=message_data.get("MessageId"),
|
|
number=message_data.get("Number"),
|
|
raw=raw,
|
|
)
|
|
|
|
def get_delivery(self, job_id: str) -> dict[str, Any]:
|
|
return self._get(
|
|
"/api/mt/GetDelivery",
|
|
{"user": self.user, "password": self.password, "jobid": job_id},
|
|
)
|
|
|
|
def wait_for_delivery(
|
|
self,
|
|
job_id: str,
|
|
*,
|
|
timeout_seconds: int = 15,
|
|
poll_interval_seconds: float = 3.0,
|
|
) -> dict[str, Any]:
|
|
deadline = time.time() + timeout_seconds
|
|
last: dict[str, Any] = {}
|
|
while time.time() < deadline:
|
|
last = self.get_delivery(job_id)
|
|
reports = last.get("DeliveryReports") or []
|
|
if reports and reports[0].get("DeliveryStatus") == "Delivered":
|
|
return last
|
|
time.sleep(poll_interval_seconds)
|
|
return last
|
|
```
|
|
|
|
### Template registry example
|
|
|
|
```python
|
|
TEMPLATES = {
|
|
"urmi_otp": DltTemplate(
|
|
sender_id="URMIAI",
|
|
pe_id="1701178780498045011",
|
|
dlt_template_id="1777178797989033323",
|
|
channel=SmsChannel.TRANSACTIONAL,
|
|
),
|
|
"webxion_password": DltTemplate(
|
|
sender_id="WEBDMG",
|
|
pe_id="1301159136621106678",
|
|
dlt_template_id="1307161537613263303",
|
|
channel=SmsChannel.TRANSACTIONAL,
|
|
),
|
|
}
|
|
|
|
|
|
def build_message(template_key: str, **variables: str) -> str:
|
|
"""Fill placeholders manually per template — keep in sync with DLT approval."""
|
|
builders = {
|
|
"urmi_otp": lambda otp, minutes: (
|
|
f"Your URMI app verification code is {otp}. "
|
|
f"It is valid for {minutes} minutes. "
|
|
f"Do not share it to anyone - URMI AI"
|
|
),
|
|
"webxion_password": lambda password: (
|
|
f"Hello Customer, Your new password is '{password}'. "
|
|
f"Thank you. Regards, WebXion"
|
|
),
|
|
}
|
|
return builders[template_key](**variables)
|
|
```
|
|
|
|
### Usage
|
|
|
|
```python
|
|
client = UltronsmsClient()
|
|
|
|
# URMI OTP
|
|
result = client.send_template_sms(
|
|
mobile="+916296346452",
|
|
message=build_message("urmi_otp", otp="482916", minutes="10"),
|
|
template=TEMPLATES["urmi_otp"],
|
|
)
|
|
|
|
# Password reset
|
|
result = client.send_template_sms(
|
|
mobile="+919149154337",
|
|
message=build_message("webxion_password", password="Kx9#mP2q"),
|
|
template=TEMPLATES["webxion_password"],
|
|
)
|
|
|
|
if result.success:
|
|
delivery = client.wait_for_delivery(result.job_id)
|
|
print(delivery)
|
|
else:
|
|
print(result.raw) # handle ErrorCode / ErrorMessage
|
|
```
|
|
|
|
---
|
|
|
|
## cURL template
|
|
|
|
Replace `{...}` placeholders at call time.
|
|
|
|
```bash
|
|
curl -sS -L -G "https://ultronsms.com/api/mt/SendSMS" \
|
|
--data-urlencode "user=${ULTRONSMS_USER}" \
|
|
--data-urlencode "password=${ULTRONSMS_PASSWORD}" \
|
|
--data-urlencode "senderid={SENDER_ID}" \
|
|
--data-urlencode "channel=Trans" \
|
|
--data-urlencode "DCS=0" \
|
|
--data-urlencode "flashsms=0" \
|
|
--data-urlencode "number={MOBILE_91XXXXXXXXXX}" \
|
|
--data-urlencode "text={FILLED_DLT_MESSAGE}" \
|
|
--data-urlencode "route=2" \
|
|
--data-urlencode "peid={PE_ID}" \
|
|
--data-urlencode "dlttemplateid={DLT_TEMPLATE_ID}"
|
|
```
|
|
|
|
**Delivery check**
|
|
|
|
```bash
|
|
curl -sS -L -G "https://ultronsms.com/api/mt/GetDelivery" \
|
|
--data-urlencode "user=${ULTRONSMS_USER}" \
|
|
--data-urlencode "password=${ULTRONSMS_PASSWORD}" \
|
|
--data-urlencode "jobid={JOB_ID}"
|
|
```
|
|
|
|
---
|
|
|
|
## Response handling
|
|
|
|
### Success criteria
|
|
|
|
| API | Success signal |
|
|
|-----|----------------|
|
|
| SendSMS | `ErrorCode == "000"`, non-empty `JobId` |
|
|
| GetDelivery | `DeliveryReports[0].DeliveryStatus == "Delivered"`, `ErrorCode == "000"` |
|
|
|
|
### Common errors
|
|
|
|
| ErrorCode | Meaning | Action |
|
|
|-----------|---------|--------|
|
|
| `7` | Invalid username/password | Check env credentials |
|
|
| `25` | Server NullReferenceException | Usually invalid `route` or malformed request |
|
|
| Non-`000` on delivery | Operator / DLT rejection | Verify template text, PE ID, sender binding |
|
|
|
|
---
|
|
|
|
## Adding a new template
|
|
|
|
1. Register header and content on the DLT portal; wait for **Approved** status.
|
|
2. Note **Sender ID**, **PE ID**, and **Template ID** from the Ultronsms dashboard.
|
|
3. Add a `DltTemplate` entry to your registry.
|
|
4. Implement a message builder that produces text identical to the approved template (variables filled only).
|
|
5. Send one test SMS to a real number; confirm delivery via `GetDelivery`.
|
|
6. Only then enable in production.
|
|
|
|
---
|
|
|
|
## Security notes
|
|
|
|
- Load `ULTRONSMS_PASSWORD` from secrets manager or `.env` (gitignored).
|
|
- Never expose gateway credentials to frontend or mobile apps.
|
|
- Log `JobId` and `MessageId` for support; avoid logging full message content if it contains OTPs or passwords.
|
|
- Use the demo account (`GENZENITHDemo`) for staging only.
|
|
|
|
---
|
|
|
|
## Related document
|
|
|
|
- [OTP_SMS_IMPLEMENTATION.md](./OTP_SMS_IMPLEMENTATION.md) — URMI OTP flow with all values hardcoded for quick drop-in use.
|