OTP send
This commit is contained in:
parent
e98036cd31
commit
614cf1e7f5
4 changed files with 398 additions and 15 deletions
310
URMIOTP_API.md
Normal file
310
URMIOTP_API.md
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
# UrmiOTP — developer API
|
||||||
|
|
||||||
|
Unified OTP API for Urmi developers. Use this document to integrate OTP delivery into your application.
|
||||||
|
|
||||||
|
**Related docs**
|
||||||
|
|
||||||
|
| Document | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| [OTP_SMS_IMPLEMENTATION.md](OTP_SMS_IMPLEMENTATION.md) | Direct Ultronsms gateway integration (standalone, not used by UrmiOTP API today) |
|
||||||
|
| [GENERIC_TEMPLATE_SMS.md](GENERIC_TEMPLATE_SMS.md) | Generic DLT template SMS via Ultronsms |
|
||||||
|
| `/api/v1/messages/docs/send` | Per-account DLT SMS API (actually delivers SMS today) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it is implemented
|
||||||
|
|
||||||
|
UrmiOTP is a **project-gated, template-based OTP layer** on top of the Urmi control-plane API. It is **not** a standalone SMS gateway integration.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐ POST /api/v1/otp/generate ┌─────────────────┐
|
||||||
|
│ Developer app │ ────────────────────────────► │ routes_otp.py │
|
||||||
|
│ (API key auth) │ └────────┬────────┘
|
||||||
|
└──────────────────┘ │
|
||||||
|
▼
|
||||||
|
┌──────────────────┐ POST /api/v1/urmiotp/generate ┌─────────────────┐
|
||||||
|
│ Urmi console │ ───────────────────────────────►│ routes_urmiotp │
|
||||||
|
│ (session auth) │ └────────┬────────┘
|
||||||
|
└──────────────────┘ │
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ otp_service.py │
|
||||||
|
│ generate_otp_ │
|
||||||
|
│ for_user() │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────────────────────┼────────────────────────┐
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
urmiotp_catalog.py UrmiOtpProject normalize_recipients() record_otp_log()
|
||||||
|
(7 hardcoded (approved project (phone formatting) → otp_message_logs
|
||||||
|
templates) lookup)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both entry points call the same core function: `generate_otp_for_user()` in `backend/app/otp_service.py`.
|
||||||
|
|
||||||
|
### Onboarding flow
|
||||||
|
|
||||||
|
1. **Developer** creates an UrmiOTP project in the console (`POST /api/v1/urmiotp/projects`).
|
||||||
|
- Provides `project_name`, `message_label` (product name shown in SMS), `app_type`, `app_url`, and `purpose`.
|
||||||
|
- Project starts with `status: pending`.
|
||||||
|
2. **Admin** reviews and approves the project (`PATCH /api/v1/admin/compliance/urmiotp-projects/{id}`).
|
||||||
|
- Only `approved` projects can call the generate API.
|
||||||
|
3. **Developer** creates an API key (`urmi_live_...`) under **Develop → API Keys**.
|
||||||
|
4. **Developer** calls `POST /api/v1/otp/generate` with the project name, template type, recipient, and OTP variables.
|
||||||
|
|
||||||
|
Per-project documentation (templates with your `message_label` filled in) is available at:
|
||||||
|
|
||||||
|
`GET /api/v1/urmiotp/projects/{project_id}/documentation`
|
||||||
|
|
||||||
|
(Requires console session auth; available only after approval.)
|
||||||
|
|
||||||
|
### Message composition
|
||||||
|
|
||||||
|
Templates are defined in `backend/app/urmiotp_catalog.py` — seven fixed DLT-style message patterns. Each template has:
|
||||||
|
|
||||||
|
- `{message_label}` — replaced with the project's registered label (e.g. `Acme App`)
|
||||||
|
- `{#var#}` × 2 — replaced in order with `variables.otp` and `variables.validity_minutes`
|
||||||
|
|
||||||
|
Example for `login_authentication` with label `Acme`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Your URMI login verification code for Acme is 583921. It is valid for 10 minutes. - URMI
|
||||||
|
```
|
||||||
|
|
||||||
|
**Urmi does not generate the OTP code.** Your backend must create the numeric code, pass it in `variables.otp`, store it server-side with a TTL, and verify it when the user submits it.
|
||||||
|
|
||||||
|
### What happens on generate
|
||||||
|
|
||||||
|
When `POST /api/v1/otp/generate` is called, `otp_service.py`:
|
||||||
|
|
||||||
|
1. Validates `template_type` against the catalog.
|
||||||
|
2. Loads the caller's **approved** `UrmiOtpProject` by `project_name`.
|
||||||
|
3. Normalizes the recipient number (10-digit Indian numbers get `91` prefix).
|
||||||
|
4. Builds the final SMS text via `_build_otp_message()`.
|
||||||
|
5. Writes a row to `otp_message_logs` with `status: accepted`.
|
||||||
|
6. Returns `message_preview` and `log_id`.
|
||||||
|
|
||||||
|
### SMS delivery
|
||||||
|
|
||||||
|
`generate_otp_for_user()` sends SMS via `UltronsmsClient.send_template_sms()` using the DLT-approved template from [OTP_SMS_IMPLEMENTATION.md](OTP_SMS_IMPLEMENTATION.md):
|
||||||
|
|
||||||
|
```
|
||||||
|
Your URMI app verification code is {otp} for {message_label}. It is valid for {validity_minutes} minutes. Do not share it to anyone - URMI AI
|
||||||
|
```
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---------|-------|
|
||||||
|
| Sender ID | `URMIAI` |
|
||||||
|
| PE ID | `1701178780498045011` |
|
||||||
|
| DLT template ID | `1777178797989033323` |
|
||||||
|
|
||||||
|
`template_type` (e.g. `login_authentication`) is used for logging and analytics only. All types deliver the same DLT-registered SMS body.
|
||||||
|
|
||||||
|
Credentials come from `ULTRONSMS_USER` and `ULTRONSMS_PASSWORD` in the server environment.
|
||||||
|
|
||||||
|
### Observability
|
||||||
|
|
||||||
|
OTP requests are stored in `otp_message_logs` and surfaced in:
|
||||||
|
|
||||||
|
- **Develop → Logs** (`GET /api/v1/logs/otp`)
|
||||||
|
- **Develop → Analytics** (grouped by project and template type)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API reference
|
||||||
|
|
||||||
|
### Generate OTP
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Method** | `POST` |
|
||||||
|
| **Path** | `/api/v1/otp/generate` |
|
||||||
|
| **Auth** | API key (`Authorization: Bearer urmi_live_...` or `X-Api-Key: urmi_live_...`) |
|
||||||
|
| **Content-Type** | `application/json` |
|
||||||
|
|
||||||
|
#### Request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project_name": "acme-login",
|
||||||
|
"template_type": "login_authentication",
|
||||||
|
"to": "919999999999",
|
||||||
|
"variables": {
|
||||||
|
"otp": "583921",
|
||||||
|
"validity_minutes": "10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `project_name` | string | yes | Your approved UrmiOTP project name (3–30 chars) |
|
||||||
|
| `template_type` | string | yes | One of the template types below |
|
||||||
|
| `to` | string | yes | Recipient mobile with country code (10–15 digits) |
|
||||||
|
| `variables.otp` | string | yes | OTP value to embed in the message |
|
||||||
|
| `variables.validity_minutes` | string | yes | Validity duration in minutes |
|
||||||
|
|
||||||
|
#### Template types
|
||||||
|
|
||||||
|
| `template_type` | Use case |
|
||||||
|
|-----------------|----------|
|
||||||
|
| `general_verification` | Signup, KYC, general verification |
|
||||||
|
| `login_authentication` | Login, MFA, 2FA |
|
||||||
|
| `account_recovery` | Forgot password, account recovery |
|
||||||
|
| `account_change` | Change phone, email, or password |
|
||||||
|
| `action_authorization` | Sensitive actions, API key changes |
|
||||||
|
| `transaction_verification` | Payments, transfers |
|
||||||
|
| `device_verification` | Device pairing, IoT activation |
|
||||||
|
|
||||||
|
#### Success response (`200`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"log_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"project_name": "acme-login",
|
||||||
|
"template_type": "login_authentication",
|
||||||
|
"to": "919999999999",
|
||||||
|
"message_preview": "Your URMI app verification code is 583921. It is valid for 10 minutes. Do not share it to anyone - URMI AI",
|
||||||
|
"status": "sent",
|
||||||
|
"job_id": "215488915",
|
||||||
|
"message_id": "5h6krUyQsU64iM0otKX7kA"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error responses
|
||||||
|
|
||||||
|
| HTTP | Cause |
|
||||||
|
|------|-------|
|
||||||
|
| `401` | Missing or invalid API key |
|
||||||
|
| `403` | Account inactive |
|
||||||
|
| `404` | UrmiOTP project not found for this user |
|
||||||
|
| `422` | Project not approved, unknown `template_type`, missing `variables`, or invalid phone number |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Create an API key in the Urmi console under **Develop → API Keys**.
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer urmi_live_<your_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternate header:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Api-Key: urmi_live_<your_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
API keys use the `urmi_live_` prefix. Implementation: `backend/app/api_key_auth.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://urmiai.com/api/v1/otp/generate" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer urmi_live_YOUR_KEY_HERE" \
|
||||||
|
-d '{
|
||||||
|
"project_name": "acme-login",
|
||||||
|
"template_type": "login_authentication",
|
||||||
|
"to": "919999999999",
|
||||||
|
"variables": {
|
||||||
|
"otp": "583921",
|
||||||
|
"validity_minutes": "10"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Local development base URL: `http://localhost:8000`
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_BASE = "https://urmiai.com"
|
||||||
|
API_KEY = "urmi_live_YOUR_KEY_HERE"
|
||||||
|
|
||||||
|
response = httpx.post(
|
||||||
|
f"{API_BASE}/api/v1/otp/generate",
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
json={
|
||||||
|
"project_name": "acme-login",
|
||||||
|
"template_type": "login_authentication",
|
||||||
|
"to": "919999999999",
|
||||||
|
"variables": {"otp": "583921", "validity_minutes": "10"},
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
print(response.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch("https://urmiai.com/api/v1/otp/generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer urmi_live_YOUR_KEY_HERE",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_name: "acme-login",
|
||||||
|
template_type: "login_authentication",
|
||||||
|
to: "919999999999",
|
||||||
|
variables: { otp: "583921", validity_minutes: "10" },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Console testing (no API key)
|
||||||
|
|
||||||
|
For testing inside the Urmi console or **Develop → Testing Center**, use the session-authenticated variant:
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Method** | `POST` |
|
||||||
|
| **Path** | `/api/v1/urmiotp/generate` |
|
||||||
|
| **Auth** | Console session cookie |
|
||||||
|
|
||||||
|
Same request body as `/api/v1/otp/generate`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration checklist
|
||||||
|
|
||||||
|
- [ ] Create and get an UrmiOTP project approved
|
||||||
|
- [ ] Create an API key (`urmi_live_...`)
|
||||||
|
- [ ] Generate OTP codes on **your** backend (Urmi does not generate them)
|
||||||
|
- [ ] Store OTP server-side with TTL matching `validity_minutes`
|
||||||
|
- [ ] Verify user-submitted OTP on your backend before marking session verified
|
||||||
|
- [ ] Rate-limit OTP requests per phone number and IP
|
||||||
|
- [ ] Do not return the OTP in your own API responses to the client
|
||||||
|
- [ ] Do not log full OTP values in application logs
|
||||||
|
- [ ] Handle non-`200` responses with a generic user message ("Unable to send OTP, try again")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `backend/app/routes_otp.py` | External API route (`/api/v1/otp/generate`) |
|
||||||
|
| `backend/app/routes_urmiotp.py` | Console routes (projects, docs, session generate) |
|
||||||
|
| `backend/app/otp_service.py` | Core generate logic |
|
||||||
|
| `backend/app/urmiotp_catalog.py` | Template catalog and API metadata |
|
||||||
|
| `backend/app/log_service.py` | `record_otp_log()` → `otp_message_logs` |
|
||||||
|
| `backend/app/models.py` | `UrmiOtpProject`, `OtpMessageLog` models |
|
||||||
|
| `backend/app/api_key_auth.py` | API key authentication |
|
||||||
|
| `frontend/src/features/agent/UrmiOtpDocsPage.tsx` | In-console API documentation UI |
|
||||||
|
|
@ -6,7 +6,8 @@ from sqlalchemy.orm import Session
|
||||||
from .log_service import record_otp_log
|
from .log_service import record_otp_log
|
||||||
from .message_send_service import SendMessageError, normalize_recipients
|
from .message_send_service import SendMessageError, normalize_recipients
|
||||||
from .models import UrmiOtpProject, UserAccount
|
from .models import UrmiOtpProject, UserAccount
|
||||||
from .urmiotp_catalog import OTP_TEMPLATES
|
from .ultronsms_client import UltronsmsClient
|
||||||
|
from .urmiotp_catalog import OTP_TEMPLATES, URMI_OTP_SMS_CONFIG, build_dlt_otp_message
|
||||||
|
|
||||||
TEMPLATE_TYPES = {template["type"] for template in OTP_TEMPLATES}
|
TEMPLATE_TYPES = {template["type"] for template in OTP_TEMPLATES}
|
||||||
|
|
||||||
|
|
@ -18,11 +19,7 @@ class GenerateOtpPayload(BaseModel):
|
||||||
variables: dict[str, str] = Field(default_factory=dict)
|
variables: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
def _build_otp_message(template_type: str, message_label: str, variables: dict[str, str]) -> str:
|
def _require_otp_variables(variables: dict[str, str]) -> None:
|
||||||
template = next((item for item in OTP_TEMPLATES if item["type"] == template_type), None)
|
|
||||||
if not template:
|
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Unknown template type")
|
|
||||||
|
|
||||||
otp_value = variables.get("otp", "").strip()
|
otp_value = variables.get("otp", "").strip()
|
||||||
validity = variables.get("validity_minutes", "").strip()
|
validity = variables.get("validity_minutes", "").strip()
|
||||||
if not otp_value or not validity:
|
if not otp_value or not validity:
|
||||||
|
|
@ -31,11 +28,6 @@ def _build_otp_message(template_type: str, message_label: str, variables: dict[s
|
||||||
detail="variables.otp and variables.validity_minutes are required",
|
detail="variables.otp and variables.validity_minutes are required",
|
||||||
)
|
)
|
||||||
|
|
||||||
content = template["content"].replace("{message_label}", message_label)
|
|
||||||
for value in (otp_value, validity):
|
|
||||||
content = content.replace("{#var#}", value, 1)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def generate_otp_for_user(
|
def generate_otp_for_user(
|
||||||
db: Session,
|
db: Session,
|
||||||
|
|
@ -68,7 +60,52 @@ def generate_otp_for_user(
|
||||||
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
|
||||||
|
|
||||||
recipient = recipients[0]
|
recipient = recipients[0]
|
||||||
message = _build_otp_message(payload.template_type, project.message_label, payload.variables)
|
_require_otp_variables(payload.variables)
|
||||||
|
try:
|
||||||
|
message = build_dlt_otp_message(payload.variables, project.message_label)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
client = UltronsmsClient()
|
||||||
|
try:
|
||||||
|
send_result = client.send_template_sms(
|
||||||
|
mobile=recipient,
|
||||||
|
message=message,
|
||||||
|
sender_id=URMI_OTP_SMS_CONFIG["sender_id"],
|
||||||
|
pe_id=URMI_OTP_SMS_CONFIG["pe_id"],
|
||||||
|
dlt_template_id=URMI_OTP_SMS_CONFIG["dlt_template_id"],
|
||||||
|
sender_type="transactional",
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
entry = record_otp_log(
|
||||||
|
db,
|
||||||
|
user,
|
||||||
|
project_name=project.project_name,
|
||||||
|
template_type=payload.template_type,
|
||||||
|
to_number=recipient,
|
||||||
|
status="failed",
|
||||||
|
error_message=str(exc),
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=str(exc),
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not send_result.success:
|
||||||
|
error_message = send_result.raw.get("ErrorMessage") or "SMS gateway rejected the request"
|
||||||
|
entry = record_otp_log(
|
||||||
|
db,
|
||||||
|
user,
|
||||||
|
project_name=project.project_name,
|
||||||
|
template_type=payload.template_type,
|
||||||
|
to_number=recipient,
|
||||||
|
status="failed",
|
||||||
|
error_message=error_message,
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail=error_message,
|
||||||
|
)
|
||||||
|
|
||||||
entry = record_otp_log(
|
entry = record_otp_log(
|
||||||
db,
|
db,
|
||||||
|
|
@ -76,7 +113,9 @@ def generate_otp_for_user(
|
||||||
project_name=project.project_name,
|
project_name=project.project_name,
|
||||||
template_type=payload.template_type,
|
template_type=payload.template_type,
|
||||||
to_number=recipient,
|
to_number=recipient,
|
||||||
status="accepted",
|
status="sent",
|
||||||
|
job_id=send_result.job_id,
|
||||||
|
message_id=send_result.message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -84,7 +123,9 @@ def generate_otp_for_user(
|
||||||
"log_id": str(entry.id),
|
"log_id": str(entry.id),
|
||||||
"project_name": project.project_name,
|
"project_name": project.project_name,
|
||||||
"template_type": payload.template_type,
|
"template_type": payload.template_type,
|
||||||
"to": recipient,
|
"to": send_result.number or recipient,
|
||||||
"message_preview": message,
|
"message_preview": message,
|
||||||
"status": entry.status,
|
"status": entry.status,
|
||||||
|
"job_id": send_result.job_id,
|
||||||
|
"message_id": send_result.message_id,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,35 @@ def render_templates_for_project(message_label: str) -> list[dict]:
|
||||||
return rendered
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
# DLT-approved SMS body for Ultronsms (template ID 1777178797989033323).
|
||||||
|
# All UrmiOTP template_type values are delivered using this registered text.
|
||||||
|
URMI_OTP_SMS_CONFIG = {
|
||||||
|
"sender_id": "URMIAI",
|
||||||
|
"pe_id": "1701178780498045011",
|
||||||
|
"dlt_template_id": "1777178797989033323",
|
||||||
|
"message_template": (
|
||||||
|
"Your URMI app verification code is {otp} for {message_label}. "
|
||||||
|
"It is valid for {validity_minutes} minutes. "
|
||||||
|
"Do not share it to anyone - URMI AI"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_dlt_otp_message(variables: dict[str, str], message_label: str) -> str:
|
||||||
|
otp_value = variables.get("otp", "").strip()
|
||||||
|
validity = variables.get("validity_minutes", "").strip()
|
||||||
|
label = message_label.strip()
|
||||||
|
if not otp_value or not validity:
|
||||||
|
raise ValueError("variables.otp and variables.validity_minutes are required")
|
||||||
|
if not label:
|
||||||
|
raise ValueError("message_label is required")
|
||||||
|
return URMI_OTP_SMS_CONFIG["message_template"].format(
|
||||||
|
otp=otp_value,
|
||||||
|
message_label=label,
|
||||||
|
validity_minutes=validity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
OTP_API_DOCUMENTATION = {
|
OTP_API_DOCUMENTATION = {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/otp/generate",
|
"path": "/api/v1/otp/generate",
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,10 @@ export function UrmiOtpDocsPage() {
|
||||||
<div className="explorer-field-row"><span>Endpoint</span><code>{docs.api.path}</code></div>
|
<div className="explorer-field-row"><span>Endpoint</span><code>{docs.api.path}</code></div>
|
||||||
<div className="otp-api-notice">
|
<div className="otp-api-notice">
|
||||||
<Code2 size={16} />
|
<Code2 size={16} />
|
||||||
<span>This is documentation only. The UrmiOTP API is not enabled yet.</span>
|
<span>
|
||||||
|
SMS delivery uses the DLT-approved URMI OTP template. <code>template_type</code> is for
|
||||||
|
categorization; the sent text matches <code>message_preview</code> in the API response.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="otp-parameter-list">
|
<div className="otp-parameter-list">
|
||||||
{Object.entries(docs.api.parameters).map(([name, description]) => (
|
{Object.entries(docs.api.parameters).map(([name, description]) => (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue