engagelab-otp
Call EngageLab OTP REST APIs to send one-time passwords (OTP), verify codes, send custom messages, and manage OTP templates across SMS, WhatsApp, Email, and Voice channels. Use this skill whenever the user wants to send an OTP or verification code via EngageLab, verify an OTP code, send custom messages through the OTP platform, manage OTP templates, configure callback webhooks, or integrate via SMPP. Also trigger when the user mentions "engagelab otp", "otp api", "send otp", "verify otp", "one-time password", "verification code", "engagelab verification", "otp template", "multi-channel otp", "whatsapp otp", "sms otp", "voice otp", "email otp", or asks to integrate with the EngageLab OTP platform. This skill covers OTP generation, delivery, verification, custom messaging, template management, callback configuration, and SMPP integration — use it even if the user only needs one of these capabilities.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-engagelab-otp
Repository
Skill path: skills/devengagelab/engagelab-otp
Call EngageLab OTP REST APIs to send one-time passwords (OTP), verify codes, send custom messages, and manage OTP templates across SMS, WhatsApp, Email, and Voice channels. Use this skill whenever the user wants to send an OTP or verification code via EngageLab, verify an OTP code, send custom messages through the OTP platform, manage OTP templates, configure callback webhooks, or integrate via SMPP. Also trigger when the user mentions "engagelab otp", "otp api", "send otp", "verify otp", "one-time password", "verification code", "engagelab verification", "otp template", "multi-channel otp", "whatsapp otp", "sms otp", "voice otp", "email otp", or asks to integrate with the EngageLab OTP platform. This skill covers OTP generation, delivery, verification, custom messaging, template management, callback configuration, and SMPP integration — use it even if the user only needs one of these capabilities.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Backend, Testing, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install engagelab-otp into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding engagelab-otp to shared team environments
- Use engagelab-otp for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: engagelab-otp
description: >
Call EngageLab OTP REST APIs to send one-time passwords (OTP), verify codes,
send custom messages, and manage OTP templates across SMS, WhatsApp, Email,
and Voice channels. Use this skill whenever the user wants to send an OTP or
verification code via EngageLab, verify an OTP code, send custom messages
through the OTP platform, manage OTP templates, configure callback webhooks,
or integrate via SMPP. Also trigger when the user mentions "engagelab otp",
"otp api", "send otp", "verify otp", "one-time password", "verification code",
"engagelab verification", "otp template", "multi-channel otp", "whatsapp otp",
"sms otp", "voice otp", "email otp", or asks to integrate with the EngageLab
OTP platform. This skill covers OTP generation, delivery, verification,
custom messaging, template management, callback configuration, and SMPP
integration — use it even if the user only needs one of these capabilities.
---
# EngageLab OTP API Skill
This skill enables interaction with the EngageLab OTP REST API. The OTP service handles one-time password generation, multi-channel delivery (SMS, WhatsApp, Email, Voice), verification, and fraud monitoring.
It covers six areas:
1. **OTP Send** — Platform-generated OTP code delivery
2. **Custom OTP Send** — User-generated OTP code delivery
3. **OTP Verify** — Validate OTP codes
4. **Custom Messages Send** — Send custom template messages (notifications, marketing)
5. **Template Management** — Create, list, get, and delete OTP templates
6. **Callback & SMPP** — Webhook configuration and SMPP protocol integration
## Resources
### scripts/
- **`otp_client.py`** — Python client class (`EngageLabOTP`) wrapping all API endpoints: `send_otp()`, `send_custom_otp()`, `verify()`, `send_custom_message()`, and template CRUD. Handles authentication, request construction, and typed error handling. Use this as a ready-to-run helper or import it into the user's project.
- **`verify_callback.py`** — HMAC-SHA256 signature verifier for incoming OTP callback webhooks. Parses the `X-CALLBACK-ID` header and validates authenticity. Drop into any web framework (Flask, FastAPI, Django) to secure callback endpoints.
### references/
- **`error-codes.md`** — Complete error code tables for all OTP APIs
- **`template-api.md`** — Full template CRUD specs with all channel configurations
- **`callback-config.md`** — Webhook setup, security, and all event types
- **`smpp-guide.md`** — SMPP protocol connection, messaging, and delivery reports
## Authentication
All EngageLab OTP API calls use **HTTP Basic Authentication**.
- **Base URL**: `https://otp.api.engagelab.cc`
- **Header**: `Authorization: Basic <base64(dev_key:dev_secret)>`
- **Content-Type**: `application/json`
The user must provide their `dev_key` and `dev_secret`. Encode them as `base64("dev_key:dev_secret")` and set the `Authorization` header.
**Example** (using curl):
```bash
curl -X POST https://otp.api.engagelab.cc/v1/messages \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'YOUR_DEV_KEY:YOUR_DEV_SECRET' | base64)" \
-d '{ ... }'
```
If the user hasn't provided credentials, ask them for their `dev_key` and `dev_secret` before generating API calls.
## Quick Reference — All Endpoints
| Operation | Method | Path |
|-----------|--------|------|
| Send OTP (platform-generated) | `POST` | `/v1/messages` |
| Send OTP (custom code) | `POST` | `/v1/codes` |
| Verify OTP | `POST` | `/v1/verifications` |
| Send custom message | `POST` | `/v1/custom-messages` |
| List templates | `GET` | `/v1/template-configs` |
| Get template details | `GET` | `/v1/template-configs/:templateId` |
| Create template | `POST` | `/v1/template-configs` |
| Delete template | `DELETE` | `/v1/template-configs/:templateId` |
## OTP Send (Platform-Generated Code)
Use this when you want **EngageLab to generate** the verification code and deliver it according to the template's channel strategy (SMS, WhatsApp, Email, Voice).
**Endpoint**: `POST /v1/messages`
### Request Body
```json
{
"to": "+8618701235678",
"template": {
"id": "test-template-1",
"language": "default",
"params": {
"key1": "value1"
}
}
}
```
### Parameters
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | `string` | Yes | Phone number (with country code, e.g., `+8613800138000`) or email address |
| `template.id` | `string` | Yes | Template ID |
| `template.language` | `string` | No | Language: `default`, `zh_CN`, `zh_HK`, `en`, `ja`, `th`, `es`. Defaults to `default` |
| `template.params` | `object` | No | Custom template variable values as key-value pairs |
### Notes on `params`
- Preset variables like `{{brand_name}}`, `{{ttl}}`, `{{pwa_url}}` are auto-filled from template settings — no need to pass them.
- For custom variables in the template (e.g., `Hi {{name}}, your code is {{code}}`), pass values via params: `{"name": "Bob"}`.
- If the template's `from_id` field is preset and you pass `{"from_id": "12345"}`, the preset value is overridden.
### Response
**Success**:
```json
{
"message_id": "1725407449772531712",
"send_channel": "sms"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `message_id` | `string` | Unique message ID, used for verification and tracking |
| `send_channel` | `string` | Current delivery channel: `whatsapp`, `sms`, `email`, or `voice` |
The `send_channel` indicates the initial channel — if fallback is configured (e.g., WhatsApp → SMS), the final delivery channel may differ.
For error codes, read `references/error-codes.md`.
## Custom OTP Send (User-Generated Code)
Use this when you want to **generate the verification code yourself** and have EngageLab deliver it. This API does not generate codes and does not require calling the Verify API afterward.
**Endpoint**: `POST /v1/codes`
### Request Body
```json
{
"to": "+8618701235678",
"code": "398210",
"template": {
"id": "test-template-1",
"language": "default",
"params": {
"key1": "value1"
}
}
}
```
### Parameters
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | `string` | Yes | Phone number or email address |
| `code` | `string` | Yes | Your custom verification code |
| `template.id` | `string` | Yes | Template ID |
| `template.language` | `string` | No | Language (same options as OTP Send). Defaults to `default` |
| `template.params` | `object` | No | Custom template variable values |
### Response
Same format as OTP Send — returns `message_id` and `send_channel`.
## OTP Verify
Validate a verification code that was sent via the OTP Send API (`/v1/messages`). Only applicable to platform-generated codes — not needed for Custom OTP Send.
**Endpoint**: `POST /v1/verifications`
### Request Body
```json
{
"message_id": "1725407449772531712",
"verify_code": "667090"
}
```
### Parameters
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message_id` | `string` | Yes | The `message_id` returned by `/v1/messages` |
| `verify_code` | `string` | Yes | The code the user entered |
### Response
**Success**:
```json
{
"message_id": "1725407449772531712",
"verify_code": "667090",
"verified": true
}
```
| Field | Type | Description |
|-------|------|-------------|
| `message_id` | `string` | Message ID |
| `verify_code` | `string` | The submitted code |
| `verified` | `boolean` | `true` = valid, `false` = invalid |
**Important**: Successfully verified codes cannot be verified again — subsequent attempts will fail. Expired codes return error `3003`.
## Custom Messages Send
Send custom template content (verification codes, notifications, or marketing messages) using templates created on the OTP platform.
**Endpoint**: `POST /v1/custom-messages`
### Request Body
```json
{
"to": "+8618701235678",
"template": {
"id": "test-template-1",
"params": {
"code": "123456",
"var1": "value1"
}
}
}
```
### Parameters
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | `string` or `string[]` | Yes | Single recipient (string) or multiple (array). Phone number or email |
| `template.id` | `string` | Yes | Template ID |
| `template.params` | `object` | No | Template variable values |
| `template.params.code` | `string` | Conditional | Required if template type is verification code |
### Notes on `params`
- Preset variables (`{{brand_name}}`, `{{ttl}}`, `{{pwa_url}}`) are auto-replaced from template settings.
- For verification code templates, you **must** pass `{{code}}` or an error will occur.
- Custom variables need values via params, e.g., `{"name": "Bob"}` for `Hi {{name}}`.
### Response
Same format as OTP Send — returns `message_id` and `send_channel`.
### Use Case Examples
**Verification code**:
```json
{
"to": "+8618701235678",
"template": { "id": "code-template", "params": { "code": "123456" } }
}
```
**Notification**:
```json
{
"to": ["+8618701235678"],
"template": { "id": "notification-template", "params": { "order": "123456" } }
}
```
**Marketing**:
```json
{
"to": ["+8618701235678"],
"template": { "id": "marketing-template", "params": { "name": "EngageLab", "promotion": "30%" } }
}
```
## Template Management
OTP templates define the message content, channel strategy, and verification code settings. Templates support multi-channel delivery with fallback (e.g., WhatsApp → SMS).
For full request/response details including all channel configurations (WhatsApp, SMS, Voice, Email, PWA), read `references/template-api.md`.
### Quick Summary
**Create** — `POST /v1/template-configs`
```json
{
"template_id": "my-template",
"description": "Login verification",
"send_channel_strategy": "whatsapp|sms",
"brand_name": "MyApp",
"verify_code_config": {
"verify_code_type": 1,
"verify_code_len": 6,
"verify_code_ttl": 5
},
"sms_config": {
"template_type": 1,
"template_default_config": {
"contain_security": false,
"contain_expire": false
}
}
}
```
**List all** — `GET /v1/template-configs` (returns array of templates without detailed content)
**Get details** — `GET /v1/template-configs/:templateId` (returns full template with channel configs)
**Delete** — `DELETE /v1/template-configs/:templateId`
### Template Status
| Value | Status |
|-------|--------|
| 1 | Pending Review |
| 2 | Approved |
| 3 | Rejected |
### Channel Strategy
Use `|` to define fallback chains. Examples:
- `"sms"` — SMS only
- `"whatsapp|sms"` — Try WhatsApp first, fall back to SMS
- `"whatsapp|sms|email"` — WhatsApp → SMS → Email
Supported channels: `whatsapp`, `sms`, `voice`, `email`
### Verification Code Config
| Field | Range | Description |
|-------|-------|-------------|
| `verify_code_type` | 1–7 | 1=Numeric, 2=Lowercase, 4=Uppercase. Combine: 3=Numeric+Lowercase |
| `verify_code_len` | 4–10 | Code length |
| `verify_code_ttl` | 1–10 | Validity in minutes. With WhatsApp: only 1, 5, or 10 |
## Callback Configuration
Configure webhook URLs to receive real-time delivery status, notification events, message responses, and system events.
For the full callback data structures, security mechanisms, and event types, read `references/callback-config.md`.
## SMPP Integration
For TCP-based SMPP protocol integration (used in carrier-level SMS delivery), read `references/smpp-guide.md`.
## Generating Code
When the user asks to send OTPs or manage templates, generate working code. Default to `curl` unless the user specifies a language. Supported patterns:
- **curl** — Shell commands with proper auth header
- **Python** — Using `requests` library
- **Node.js** — Using `fetch` or `axios`
- **Java** — Using `HttpClient`
- **Go** — Using `net/http`
Always include the authentication header and proper error handling. Use placeholder values like `YOUR_DEV_KEY` and `YOUR_DEV_SECRET` if the user hasn't provided credentials.
### Python Example — Send OTP and Verify
```python
import requests
import base64
DEV_KEY = "YOUR_DEV_KEY"
DEV_SECRET = "YOUR_DEV_SECRET"
BASE_URL = "https://otp.api.engagelab.cc"
auth_string = base64.b64encode(f"{DEV_KEY}:{DEV_SECRET}".encode()).decode()
headers = {
"Content-Type": "application/json",
"Authorization": f"Basic {auth_string}"
}
# Step 1: Send OTP
send_resp = requests.post(f"{BASE_URL}/v1/messages", headers=headers, json={
"to": "+8618701235678",
"template": {"id": "my-template", "language": "default"}
})
result = send_resp.json()
message_id = result["message_id"]
# Step 2: Verify OTP (after user enters the code)
verify_resp = requests.post(f"{BASE_URL}/v1/verifications", headers=headers, json={
"message_id": message_id,
"verify_code": "123456"
})
verification = verify_resp.json()
print(f"Verified: {verification['verified']}")
```
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/error-codes.md
```markdown
# EngageLab OTP API Error Codes
## OTP Sending & Verification Errors
| Error Code | HTTP Code | Description |
|------------|-----------|-------------|
| 1000 | 500 | Internal error |
| 2001 | 401 | Authentication failed — invalid or missing token |
| 2002 | 401 | Authentication failed — token expired or disabled |
| 2004 | 403 | No permission to call this API |
| 3001 | 400 | Invalid request parameter format (not valid JSON) |
| 3002 | 400 | Invalid request parameters (do not meet requirements) |
| 3003 | 400 | Business validation failed — see `message` field for details. For verification: code expired or already verified |
| 3004 | 400 | Rate limit exceeded — cannot resend to the same template and target user within OTP validity period |
| 4001 | 400 | Resource not found (e.g., non-existent template or message ID) |
| 5001 | 400 | Sending failed (general/other) |
| 5011 | 400 | Invalid phone number format |
| 5012 | 400 | Destination unreachable |
| 5013 | 400 | Number added to blacklist |
| 5014 | 400 | Content does not comply with regulations |
| 5015 | 400 | Message intercepted/rejected |
| 5016 | 400 | Internal sending error |
| 5017 | 400 | No permission to send in China region |
| 5018 | 400 | Phone malfunction (powered off/suspended) |
| 5019 | 400 | User unsubscribed |
| 5020 | 400 | Number not registered/invalid number |
## Template API Errors
| Error Code | HTTP Code | Description |
|------------|-----------|-------------|
| 1000 | 500 | Internal error |
| 2001 | 401 | Authentication failed — incorrect token |
| 2002 | 401 | Authentication failed — token expired or disabled |
| 2004 | 403 | No permission to call this API |
| 3001 | 400 | Invalid request parameter format (not valid JSON) |
| 3002 | 400 | Invalid request parameters |
| 3003 | 400 | Business-level parameter error (e.g., missing channel config) |
| 4001 | 400 | Template does not exist |
## Error Response Format
All error responses follow this structure:
```json
{
"code": 5001,
"message": "sms send fail"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `code` | `int` | Error code from the tables above |
| `message` | `string` | Human-readable error details |
```
### references/template-api.md
```markdown
# OTP Template API Reference
Detailed request/response specifications for OTP template management endpoints.
## Table of Contents
1. [Create Template](#1-create-template)
2. [Delete Template](#2-delete-template)
3. [List All Templates](#3-list-all-templates)
4. [Get Template Details](#4-get-template-details)
---
## 1. Create Template
`POST /v1/template-configs`
### Request Body
```json
{
"template_id": "test-template-1",
"description": "Test Template 1",
"send_channel_strategy": "whatsapp|sms",
"brand_name": "Brand Name",
"verify_code_config": {
"verify_code_type": 1,
"verify_code_len": 6,
"verify_code_ttl": 1
},
"whatsapp_config": {
"template_type": 1,
"template_default_config": {
"contain_security": false,
"contain_expire": false
}
},
"sms_config": {
"template_type": 2,
"template_default_config": {
"contain_security": false,
"contain_expire": false
},
"template_custom_config": {
"custom_sub_type": "authentication",
"custom_content": "Your code is {{code}}",
"custom_country_codes": "HK,PH"
}
},
"voice_config": {
"template_type": 1,
"template_default_config": {
"contain_security": false,
"contain_expire": false
}
},
"email_config": {
"template_name": "Email Template Name",
"template_custom_configs": [{
"language": "default",
"pre_from_name": "MyApp",
"pre_from_mail": "[email protected]",
"pre_subject": "Your verification code",
"template_content": "Hi {{name}}, your verification code is {{code}}",
"pre_param_map": {
"name": "User"
}
}]
},
"pwa_config": {
"pwa_platform": "xx",
"pwa_code": "xx"
}
}
```
### Request Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `template_id` | String | Yes | Custom template ID, unique within app. Max 128 chars |
| `description` | String | No | Template description, max 255 chars |
| `send_channel_strategy` | String | Yes | Channel strategy. Single: `whatsapp`, `sms`, `email`, `voice`. Use `\|` for fallback chains (e.g., `whatsapp\|sms`) |
| `brand_name` | String | No | Brand signature, 2–10 chars. Replaces `{{brand_name}}` in templates |
| `verify_code_config` | Object | Conditional | Required when template includes verification codes |
| `verify_code_config.verify_code_type` | Integer | Yes | Code type, range [1,7]. 1=Numeric, 2=Lowercase, 4=Uppercase. Combinable (3=Numeric+Lowercase) |
| `verify_code_config.verify_code_len` | Integer | Yes | Code length, range [4,10] |
| `verify_code_config.verify_code_ttl` | Integer | Yes | Validity in minutes, range [1,10]. With WhatsApp: only 1, 5, or 10 |
### WhatsApp Config
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `whatsapp_config.template_type` | Integer | Yes | Fixed value `1` (default template only) |
| `whatsapp_config.template_default_config.contain_security` | Boolean | Yes | Include security reminder |
| `whatsapp_config.template_default_config.contain_expire` | Boolean | Yes | Include expiration content |
### SMS Config
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `sms_config.template_type` | Integer | Yes | 1=Default Template, 2=Custom Template |
| `sms_config.template_default_config` | Object | Conditional | Required for default template type (type=1) |
| `sms_config.template_default_config.contain_security` | Boolean | Yes | Include security reminder |
| `sms_config.template_default_config.contain_expire` | Boolean | Yes | Include expiration content |
| `sms_config.template_custom_config` | Object | Conditional | Required for custom template type (type=2) |
| `sms_config.template_custom_config.custom_sub_type` | String | Yes | `authentication` (verification code), `marketing`, or `utility` (notification) |
| `sms_config.template_custom_config.custom_content` | String | Yes | Template content. For `authentication` type, must include `{{code}}` |
| `sms_config.template_custom_config.custom_country_codes` | String | No | Target country codes (comma-separated), used during template review |
### Voice Config
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `voice_config.template_type` | Integer | Yes | Fixed value `1` (default template only) |
| `voice_config.template_default_config.contain_security` | Boolean | Yes | Include security reminder |
| `voice_config.template_default_config.contain_expire` | Boolean | Yes | Include expiration content |
### Email Config
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `email_config.template_name` | String | Yes | Email template name |
| `email_config.template_custom_configs` | Array | Conditional | Array of language-specific email configs |
| `.language` | String | Yes | Language: `default`, `zh_CN`, `zh_HK`, `en`, `ja`, `th`, `es` |
| `.pre_from_name` | String | No | Preset sender name |
| `.pre_from_mail` | String | Yes | Preset sender email |
| `.pre_subject` | String | Yes | Preset email subject |
| `.template_content` | String | Yes | Email content (supports HTML). Variables use `{{var}}` syntax |
| `.pre_param_map` | Object | No | Default values for template variables (key-value pairs, values must be strings) |
### PWA Config (Optional)
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `pwa_config.pwa_platform` | String | No | PWA platform (contact technical support for values) |
| `pwa_config.pwa_code` | String | No | Code in the PWA platform |
### Response
**Success**:
```json
{
"code": 0,
"message": "success"
}
```
**Failure**:
```json
{
"code": 3003,
"message": "not contains any channel config"
}
```
---
## 2. Delete Template
`DELETE /v1/template-configs/:templateId`
The `{templateId}` in the URL is the custom template ID defined when creating the template.
No request body required.
### Response
**Success**:
```json
{
"code": 0,
"message": "success"
}
```
**Failure** (template not found):
```json
{
"code": 4001,
"message": "config not exist"
}
```
---
## 3. List All Templates
`GET /v1/template-configs`
No request parameters. Returns a brief list of all templates (excludes detailed channel content). Use the Get Template Details endpoint for full configuration.
### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `template_id` | String | Custom template ID |
| `description` | String | Template description |
| `send_channel_strategy` | String | Channel strategy with `\|` fallback notation |
| `brand_name` | String | Brand name |
| `verify_code_config` | Object | Verification code settings (if applicable) |
| `whatsapp_config` | Object | WhatsApp config summary |
| `sms_config` | Object | SMS config summary (includes `template_parts` for billing) |
| `voice_config` | Object | Voice config summary |
| `email_config` | Object | Email config summary (template name only) |
| `pwa_config` | Object | PWA config (if applicable) |
| `created_time` | Integer | Creation timestamp (seconds) |
| `status` | Integer | 1=Pending Review, 2=Approved, 3=Rejected |
| `audit_remark` | String | Review remarks (e.g., rejection reason) |
### Example Response
```json
[{
"template_id": "test-template-1",
"description": "Test Template 1",
"send_channel_strategy": "whatsapp|sms",
"brand_name": "Brand Name",
"verify_code_config": {
"verify_code_type": 1,
"verify_code_len": 6,
"verify_code_ttl": 1
},
"sms_config": {
"template_type": 2,
"template_parts": 1
},
"created_time": 1234567890,
"status": 2,
"audit_remark": ""
}]
```
The `template_parts` field in `sms_config` indicates estimated billing parts — if the template content is long, billing = parts × unit price.
---
## 4. Get Template Details
`GET /v1/template-configs/:templateId`
Returns the full template configuration including detailed channel configs (custom template content, email HTML, etc.).
### Response
The response includes all fields from the List endpoint plus detailed channel configurations:
- `sms_config.template_custom_config` — Custom SMS content and sub-type
- `email_config.template_custom_configs` — Full email template configs per language
- All other channel configs with their detailed settings
### Example Response
```json
{
"template_id": "test-template-1",
"description": "Test Template 1",
"send_channel_strategy": "whatsapp|sms",
"brand_name": "Brand Name",
"verify_code_config": {
"verify_code_type": 1,
"verify_code_len": 6,
"verify_code_ttl": 1
},
"sms_config": {
"template_type": 2,
"template_parts": 1,
"template_custom_config": {
"custom_sub_type": "authentication",
"custom_content": "Your code is {{code}}"
}
},
"email_config": {
"template_name": "Email Template Name",
"template_custom_configs": [{
"language": "default",
"pre_from_name": "MyApp",
"pre_from_mail": "[email protected]",
"pre_subject": "Your verification code",
"template_content": "Hi {{name}}, your code is {{code}}",
"pre_param_map": {
"name": "User"
}
}]
},
"created_time": 1234567890,
"status": 2,
"audit_remark": ""
}
```
**Failure** (template not found):
```json
{
"code": 4001,
"message": "config not exist"
}
```
```
### references/callback-config.md
```markdown
# OTP Callback Configuration Reference
Configure webhook URLs to receive real-time events from the EngageLab OTP platform.
## Table of Contents
1. [Callback URL Setup](#callback-url-setup)
2. [Security Mechanisms](#security-mechanisms)
3. [Event Types](#event-types)
- [Message Status Events](#message-status-events)
- [Notification Events](#notification-events)
- [Message Response Events](#message-response-events)
- [System Events](#system-events)
---
## Callback URL Setup
After configuring the callback URL in the EngageLab console, the platform sends an HTTP POST request to validate availability. Your service must return HTTP 200 within 3 seconds.
**Firewall whitelist**: Add `119.8.170.74` and `114.119.180.30` to your server's firewall.
**Validation request**:
```bash
curl -X POST https://your-callback-url.com -d ''
```
**Required response**: HTTP 200 with empty body.
---
## Security Mechanisms
### Username and Secret Validation (Optional)
When configured, EngageLab includes an `X-CALLBACK-ID` header:
```
X-CALLBACK-ID: timestamp={timestamp};nonce={nonce};username={username};signature={signature}
```
**Signature verification** (Python):
```python
import hashlib, hmac
def verify(username, secret, timestamp, nonce, signature):
return signature == hmac.new(
key=secret.encode(),
msg=f'{timestamp}{nonce}{username}'.encode(),
digestmod=hashlib.sha256
).hexdigest()
```
### Authorization Authentication (Optional)
If your callback endpoint requires authentication (Basic Auth, Bearer Token), provide the Authorization info during configuration. EngageLab automatically includes it in callback requests.
---
## Event Types
All callbacks use this outer structure:
```json
{
"total": 1,
"rows": [{ /* event object */ }]
}
```
### Message Status Events
Track the lifecycle of each message with real-time delivery status updates.
| Event Identifier | Description |
|------------------|-------------|
| `plan` | Message scheduled and added to sending queue |
| `target_valid` | Target number is valid |
| `target_invalid` | Target number is invalid |
| `sent` | Message successfully sent |
| `sent_failed` | Message sending failed |
| `delivered` | Message delivered to user's device |
| `delivered_failed` | Message sent but failed to deliver |
| `verified` | User completed OTP verification |
| `verified_failed` | User verification failed |
| `verified_timeout` | User did not verify within the time limit |
**ReportLifecycle Object**:
| Field | Type | Description |
|-------|------|-------------|
| `message_id` | string | Unique message ID |
| `to` | string | Recipient phone number |
| `server` | string | Service type (e.g., `otp`) |
| `channel` | string | Channel type |
| `itime` | int64 | Callback timestamp (seconds) |
| `custom_args` | object | Custom parameters (returned if provided) |
| `status` | object | Status details |
**Status Object**:
| Field | Type | Description |
|-------|------|-------------|
| `message_status` | string | Current status (see event identifiers above) |
| `status_data` | object | Contains `msg_time`, `message_id`, `current_send_channel`, `template_key`, `business_id` |
| `billing` | object | Billing info: `cost` (float64) and `currency` ("USD"). Included for `sent` stage |
| `error_code` | int | Error code, 0 = no error |
| `error_detail` | object | Contains `message` field with error description |
**Example — Message Sent Successfully**:
```json
{
"total": 1,
"rows": [{
"message_id": "123456789",
"to": "+8613800138000",
"server": "sms",
"channel": "sms",
"itime": 1701234567,
"custom_args": { "order_id": "ORDER123" },
"status": {
"message_status": "sent",
"status_data": {
"msg_time": 1701234560,
"message_id": "123456789",
"current_send_channel": "CHANNEL_A",
"template_key": "verify_code",
"business_id": "1001"
},
"billing": { "cost": 0.005, "currency": "USD" },
"error_code": 0
}
}]
}
```
**Example — Message Failed**:
```json
{
"total": 1,
"rows": [{
"message_id": "123456790",
"to": "+8613800138001",
"server": "sms",
"channel": "sms",
"itime": 1701234568,
"status": {
"message_status": "sent_fail",
"status_data": {
"msg_time": 1701234561,
"message_id": "123456790",
"current_send_channel": "CHANNEL_B",
"template_key": "verify_code",
"business_id": "1001"
},
"error_code": 4001,
"error_detail": { "message": "Invalid phone number" }
}
}]
}
```
### Notification Events
System-level alerts for service status and account management.
| Event Identifier | Description |
|------------------|-------------|
| `insufficient_verification_rate` | Verification rate below threshold |
| `insufficient_balance` | Insufficient account balance |
| `template_audit_result` | Template audit result notification |
**Example — Insufficient Balance**:
```json
{
"total": 1,
"rows": [{
"server": "otp",
"itime": 1712458844,
"notification": {
"event": "insufficient_balance",
"notification_data": {
"business_id": "1744569418236633088",
"remain_balance": -0.005,
"balance_threshold": 2
}
}
}]
}
```
### Message Response Events
Callback events for user interactions (e.g., uplink messages).
| Event Identifier | Description |
|------------------|-------------|
| `uplink_message` | User-sent SMS or other uplink message |
**Example**:
```json
{
"total": 1,
"rows": [{
"server": "otp",
"itime": 1741083306,
"message_id": "0",
"business_id": "0",
"response": {
"event": "uplink_message",
"response_data": {
"message_sid": "SM1234567890",
"account_sid": "AC1234567890",
"from": "+1234567890",
"to": "+0987654321",
"body": "Reply message content"
}
}
}]
}
```
### System Events
Operations related to accounts, templates, API calls, and key management.
| Event Identifier | Description |
|------------------|-------------|
| `account_login` | Account login operations |
| `key_manage` | Key changes and management |
| `msg_history` | Message history queries |
| `template_manage` | Template creation, updates, deletions |
| `api_call` | API call operations |
**System Event Structure**:
```json
{
"total": 1,
"rows": [{
"server": "otp",
"itime": 1694012345,
"system_event": {
"event": "event_identifier",
"data": {
"business_id": "123",
"org_id": "org-abc",
"operator": {
"email": "[email protected]",
"api_key": "api-xxxx",
"ip_address": "1.2.3.4"
}
}
}
}]
}
```
#### Account Login Events
| Field | Type | Description |
|-------|------|-------------|
| `action` | string | `login_success`, `login_failed`, or `logout` |
| `fail_reason` | string | Only for `login_failed` |
#### Template Management Events
| Field | Type | Description |
|-------|------|-------------|
| `action` | string | `create`, `update`, or `delete` |
| `template_id` | string | Template ID |
| `template_name` | string | Template name |
#### Key Management Events
| Field | Type | Description |
|-------|------|-------------|
| `action` | string | `create`, `update`, `delete`, or `view` |
| `api_key` | string | API Key |
| `description` | string | Description (optional) |
#### API Call Events
| Field | Type | Description |
|-------|------|-------------|
| `api_path` | string | API endpoint path |
| `method` | string | HTTP method |
```
### references/smpp-guide.md
```markdown
# OTP SMPP Integration Guide
SMPP (Short Message Peer-to-Peer) protocol integration for OTP services via TCP connections.
## Table of Contents
1. [Connection Setup](#connection-setup)
2. [Message Sending](#message-sending)
3. [Delivery Reports](#delivery-reports)
4. [Connection Keep-Alive](#connection-keep-alive)
5. [Status Codes](#status-codes)
---
## Connection Setup
### SMPP Address
Obtain the following from EngageLab sales or technical support:
- **IP Address**: `{ ip }`
- **Port**: `{ port }`
- **Account**: `{ system_id }`
- **Password**: `{ password }`
### Authentication
After establishing a TCP connection, send the `BindTransceiver` command (`0x00000009`) as the first data packet:
- **system_id**: Client system account for identity verification
- **password**: Access password for authentication
### Server Response
- **Success**: `BindTransceiverResp` (`0x80000009`) with the `system_id` echoed back
- **Failure**: TCP connection is disconnected and the reason is logged
---
## Message Sending
Uses the `SUBMIT_SM` command (`0x00000004`).
### Client Data Packet Fields
| Field | Values | Description |
|-------|--------|-------------|
| `service_type` | `MSG` / `CODE` / `MRKT` | Message type. Default: `MSG` |
| `source_addr` | (blank) | Not used in OTP currently |
| `destination_addr` | e.g., `+8613800138000` | International phone number format |
| `data_coding` | `UCS2` recommended | Use UCS2 to properly parse `{}` characters |
| `header status` | `0x00000000` | Fixed value |
### Message Content (`short_message`)
JSON format inside the `short_message` field:
```json
{
"id": "template-id",
"language": "default",
"code": "123456",
"params": {
"key1": "val1"
}
}
```
| Field | Description |
|-------|-------------|
| `id` | Template ID |
| `language` | Language, default is `"default"` |
| `code` | Verification code (when `service_type` is `CODE`) |
| `params` | Custom key-value pairs (values must be strings) |
Use `UCS2` encoding (`data_coding`) to ensure `{}` characters are properly parsed. If `{}` can be sent directly, default encoding (`0x00`) may be used.
### Server Response (`SUBMIT_SM_RESP` — `0x80000004`)
**Success**:
- `header status`: `0x00000000`
- `MESSAGE_ID`: Message ID for delivery report association
**Failure status codes**:
| Status Code | Value | Description |
|-------------|-------|-------------|
| `ESME_RINVPARAM` | `0x00000032` | Invalid parameters |
| `ESME_RSYSERR` | `0x00000008` | Internal server error, retry later |
| `ESME_RSUBMITFAIL` | `0x00000045` | Delivery failed (possibly template issues) |
| `ESME_RINVNUMMSGS` | `0x00000055` | Invalid `short_message` format |
| `ESME_RUNKNOWNERR` | `0x000000FF` | Unknown error, contact support |
---
## Delivery Reports
The server reports delivery results via `DELIVER_SM` (`0x00000005`).
### Fields
| Field | Value | Description |
|-------|-------|-------------|
| `service_type` | `MSG` | Fixed |
| `source_addr` | (blank) | — |
| `destination_addr` | — | Matches `SUBMIT_SM` destination |
| `data_coding` | `0x00` | Default encoding |
| `esm_class` | `0x00` | — |
| `short_message` | delivery receipt | Contains `stat` field |
| `ReceiptedMessageID` (TLV) | — | Message ID for association |
| `MessageState` (TLV) | `0x0427` | Message state |
### Delivery Status
| Status | Meaning |
|--------|---------|
| `DELIVRD` | Delivered successfully |
| `UNDELIV` | Delivery failed |
---
## Connection Keep-Alive
Send `EnquireLink` (`0x00000015`) every **30 seconds** (±5 seconds) to maintain the connection.
---
## Supported Commands
The server only accepts these SMPP commands:
| Command | Code |
|---------|------|
| `EnquireLink` | `0x00000015` |
| `Unbind` | `0x00000006` |
| `SubmitSM` | `0x00000004` |
| `DeliverSMResp` | `0x80000005` |
| `BindTransceiver` | `0x00000009` |
Invalid commands receive `GENERIC_NACK` (`0x80000000`).
---
## Status Codes Summary
| Status Code | Meaning |
|-------------|---------|
| `ESME_RINVPARAM` | Invalid parameters |
| `ESME_RINVNUMMSGS` | Invalid `short_message` format |
| `ESME_RSUBMITFAIL` | Delivery failed (template issues) |
| `ESME_RSYSERR` | Internal server error |
| `ESME_RUNKNOWNERR` | Unknown error |
| `DELIVRD` | Successfully delivered |
| `UNDELIV` | Delivery failed |
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "devengagelab",
"slug": "engagelab-otp",
"displayName": "EngageLab OTP",
"latest": {
"version": "1.0.0",
"publishedAt": 1773196563372,
"commit": "https://github.com/openclaw/skills/commit/49d24d628e1a4bb6e4e8f7b9452420283369508b"
},
"history": []
}
```
### scripts/otp_client.py
```python
"""
EngageLab OTP API client.
Wraps all OTP REST API endpoints: send OTP, custom OTP, verify, custom messages,
and template CRUD. Handles authentication, request construction, and error handling.
Usage:
from otp_client import EngageLabOTP
client = EngageLabOTP("YOUR_DEV_KEY", "YOUR_DEV_SECRET")
# Send platform-generated OTP
result = client.send_otp("+8618701235678", "my-template")
# Verify
verification = client.verify(result["message_id"], "123456")
"""
import base64
import json
import requests
from typing import Optional, Union
BASE_URL = "https://otp.api.engagelab.cc"
class EngageLabOTPError(Exception):
"""Raised when the OTP API returns an error response."""
def __init__(self, code: int, message: str, http_status: int):
self.code = code
self.message = message
self.http_status = http_status
super().__init__(f"[{http_status}] Error {code}: {message}")
class EngageLabOTP:
"""Client for the EngageLab OTP REST API."""
def __init__(self, dev_key: str, dev_secret: str, base_url: str = BASE_URL):
auth = base64.b64encode(f"{dev_key}:{dev_secret}".encode()).decode()
self._headers = {
"Content-Type": "application/json",
"Authorization": f"Basic {auth}",
}
self._base_url = base_url.rstrip("/")
def _request(self, method: str, path: str, payload: Optional[dict] = None) -> dict:
url = f"{self._base_url}{path}"
resp = requests.request(method, url, headers=self._headers, json=payload)
if resp.status_code >= 400:
try:
body = resp.json()
except (ValueError, json.JSONDecodeError):
body = {"code": resp.status_code, "message": resp.text}
raise EngageLabOTPError(
code=body.get("code", resp.status_code),
message=body.get("message", resp.text),
http_status=resp.status_code,
)
if not resp.content:
return {}
return resp.json()
# ── OTP Send (platform-generated code) ──────────────────────────
def send_otp(
self,
to: str,
template_id: str,
language: str = "default",
params: Optional[dict] = None,
) -> dict:
"""
Send a platform-generated OTP code.
Returns dict with `message_id` and `send_channel`.
Use `message_id` with `verify()` to validate the code.
"""
template = {"id": template_id, "language": language}
if params:
template["params"] = params
return self._request("POST", "/v1/messages", {"to": to, "template": template})
# ── Custom OTP Send (user-generated code) ───────────────────────
def send_custom_otp(
self,
to: str,
code: str,
template_id: str,
language: str = "default",
params: Optional[dict] = None,
) -> dict:
"""
Send a user-generated OTP code. No verification API call needed afterward.
Returns dict with `message_id` and `send_channel`.
"""
template = {"id": template_id, "language": language}
if params:
template["params"] = params
return self._request("POST", "/v1/codes", {"to": to, "code": code, "template": template})
# ── OTP Verify ──────────────────────────────────────────────────
def verify(self, message_id: str, verify_code: str) -> dict:
"""
Verify a platform-generated OTP code.
Returns dict with `message_id`, `verify_code`, and `verified` (bool).
Successfully verified codes cannot be verified again.
"""
return self._request("POST", "/v1/verifications", {
"message_id": message_id,
"verify_code": verify_code,
})
# ── Custom Messages Send ────────────────────────────────────────
def send_custom_message(
self,
to: Union[str, list],
template_id: str,
params: Optional[dict] = None,
) -> dict:
"""
Send a custom template message (verification, notification, or marketing).
`to` can be a single recipient (str) or multiple (list).
For verification code templates, `params` must include `code`.
Returns dict with `message_id` and `send_channel`.
"""
template = {"id": template_id}
if params:
template["params"] = params
return self._request("POST", "/v1/custom-messages", {"to": to, "template": template})
# ── Template Management ─────────────────────────────────────────
def create_template(self, template_config: dict) -> dict:
"""
Create an OTP template.
`template_config` must include at minimum:
- template_id (str): unique ID
- send_channel_strategy (str): e.g. "sms", "whatsapp|sms"
- channel-specific config (sms_config, whatsapp_config, etc.)
Returns dict with `code` and `message`.
"""
return self._request("POST", "/v1/template-configs", template_config)
def list_templates(self) -> list:
"""Return a list of all templates (brief, without detailed content)."""
return self._request("GET", "/v1/template-configs")
def get_template(self, template_id: str) -> dict:
"""Return full template details including channel configurations."""
return self._request("GET", f"/v1/template-configs/{template_id}")
def delete_template(self, template_id: str) -> dict:
"""Delete a template. Returns dict with `code` and `message`."""
return self._request("DELETE", f"/v1/template-configs/{template_id}")
if __name__ == "__main__":
DEV_KEY = "YOUR_DEV_KEY"
DEV_SECRET = "YOUR_DEV_SECRET"
client = EngageLabOTP(DEV_KEY, DEV_SECRET)
# -- Send OTP and verify --
# result = client.send_otp("+8618701235678", "my-template")
# print(f"Sent! message_id={result['message_id']}, channel={result['send_channel']}")
# verification = client.verify(result["message_id"], "123456")
# print(f"Verified: {verification['verified']}")
# -- Send custom OTP (your own code) --
# result = client.send_custom_otp("+8618701235678", "398210", "my-template")
# print(f"Custom OTP sent! message_id={result['message_id']}")
# -- Send custom message (notification) --
# result = client.send_custom_message(
# ["+8618701235678"],
# "notification-template",
# params={"order": "ORD-9876"},
# )
# print(f"Notification sent! message_id={result['message_id']}")
# -- List templates --
# templates = client.list_templates()
# for t in templates:
# print(f" {t['template_id']}: {t.get('description', '')} (status={t['status']})")
pass
```
### scripts/verify_callback.py
```python
"""
EngageLab OTP callback signature verifier.
Validates the X-CALLBACK-ID header on incoming webhook requests to confirm
they originate from EngageLab. Use this in your callback endpoint handler.
Usage (Flask example):
from verify_callback import verify_engagelab_callback
@app.route("/otp-callback", methods=["POST"])
def otp_callback():
header = request.headers.get("X-CALLBACK-ID", "")
if not verify_engagelab_callback(header, "my_secret"):
return "Unauthorized", 401
data = request.get_json()
for row in data.get("rows", []):
status = row.get("status", {}).get("message_status")
print(f"Message {row['message_id']}: {status}")
return "", 200
"""
import hashlib
import hmac
from typing import Optional
def parse_callback_header(header: str) -> Optional[dict]:
"""
Parse the X-CALLBACK-ID header into its components.
Header format:
timestamp={ts};nonce={nonce};username={user};signature={sig}
Returns dict with keys: timestamp, nonce, username, signature.
Returns None if the header cannot be parsed.
"""
parts = {}
for segment in header.split(";"):
if "=" not in segment:
return None
key, value = segment.split("=", 1)
parts[key.strip()] = value.strip()
required = {"timestamp", "nonce", "username", "signature"}
if not required.issubset(parts.keys()):
return None
return parts
def compute_signature(secret: str, timestamp: str, nonce: str, username: str) -> str:
"""Compute HMAC-SHA256 signature matching EngageLab's algorithm."""
message = f"{timestamp}{nonce}{username}"
return hmac.new(
key=secret.encode(),
msg=message.encode(),
digestmod=hashlib.sha256,
).hexdigest()
def verify_engagelab_callback(header: str, secret: str) -> bool:
"""
Verify an EngageLab OTP callback request.
Args:
header: The value of the X-CALLBACK-ID HTTP header.
secret: Your configured callback secret.
Returns:
True if the signature is valid, False otherwise.
"""
parsed = parse_callback_header(header)
if parsed is None:
return False
expected = compute_signature(
secret,
parsed["timestamp"],
parsed["nonce"],
parsed["username"],
)
return hmac.compare_digest(expected, parsed["signature"])
if __name__ == "__main__":
# Quick self-test
test_secret = "my_secret"
test_ts = "1701234567"
test_nonce = "abc123"
test_user = "my_user"
sig = compute_signature(test_secret, test_ts, test_nonce, test_user)
test_header = f"timestamp={test_ts};nonce={test_nonce};username={test_user};signature={sig}"
assert verify_engagelab_callback(test_header, test_secret), "Valid signature should pass"
assert not verify_engagelab_callback(test_header, "wrong_secret"), "Wrong secret should fail"
assert not verify_engagelab_callback("malformed", test_secret), "Malformed header should fail"
print("All checks passed.")
```