# 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_ ``` Alternate header: ``` X-Api-Key: urmi_live_ ``` 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 |