first commit
This commit is contained in:
commit
efa5080897
127 changed files with 18610 additions and 0 deletions
387
GENERIC_TEMPLATE_SMS.md
Normal file
387
GENERIC_TEMPLATE_SMS.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue