Ideamart MCP — User Guide
Connect your AI coding assistant to Ideamart APIs via MCP. Send SMS, manage subscriptions, handle USSD sessions, charge subscribers, query locations, and verify identities via OTP.
MCP URL: https://mcp.ideamart.io/ideamart/mcp
Prerequisites
- MCP Token — Get one at
https://mcp.ideamart.io/auth/login - Ideamart Application ID — Your registered app ID from Ideamart portal
- Application Password — Your app's password
- IP Whitelist — MCP server IP
54.255.44.189must be whitelisted for your app
Critical Rules
These are non-negotiable. Getting any wrong causes real damage:
- Branch on
statusCode, NEVER HTTP status — Ideamart returns HTTP 200 for failures. Always checkstatusCode === "S1000". destinationAddressesis ALWAYS an ARRAY — Even for one recipient. Sending a string fails silently.- Never hardcode
applicationIdorpassword— Environment variables only. - Never retry a debit with a fresh
externalTrxId— Double-charges a real person. - Never call Ideamart from client-side code — Backend only. IP whitelisting is enforced.
- Treat
subscriberIdas opaque — With masking, it's a hash. Don't parse or assume format. - Acknowledge callbacks BEFORE doing work — Respond
{"statusCode":"S1000"}immediately. - Never disable TLS verification — No
rejectUnauthorized: false. - Guard
tel:allbroadcasts — Sanity-check base size first. - Set explicit timeouts — 10-15 seconds on every HTTP call.
- Never log secrets — Never log password, OTP, referenceNo, or unmasked subscriberId.
- Use decimal types for money — Never binary float.
- Retry only transient codes — Exponential backoff with jitter, capped, then dead-letter.
Setup
Add to your MCP client config (Kiro, Claude Desktop, Cursor, VS Code):
{
"mcpServers": {
"ideamart": {
"url": "https://mcp.ideamart.io/ideamart/mcp",
"headers": { "Authorization": "Bearer mcp_your_token_here" }
}
}
}
Credential Model
Every Ideamart API call carries credentials in the JSON body (not headers):
- applicationId — Your app ID
- password — Your app password
No OAuth, no tokens, no Authorization header. Just these two fields in every request.
Available Tools (11)
| Tool | Purpose |
|---|---|
ideamart_sms_send |
Send SMS to subscribers |
ideamart_sms_receive_parse |
Parse inbound SMS webhook payload |
ideamart_subscription_subscribe |
Subscribe a user (opt-in) |
ideamart_subscription_status_check |
Check subscription status |
ideamart_subscription_unsubscribe |
Unsubscribe a user (opt-out) |
ideamart_query_base |
Query subscriber base count |
ideamart_lbs_query |
Query subscriber location |
ideamart_ussd_send |
Send/respond to USSD session |
ideamart_ussd_receive_parse |
Parse inbound USSD webhook payload |
ideamart_ip_check |
Check MCP server's outgoing IP |
ideamart_docs |
Get API documentation by topic |
Number Masking
By DEFAULT, all Ideamart apps have number masking ENABLED. You never see real phone numbers.
Masked format: tel:hu3b84346f63899a32ec742a666503a02a4dffe4f5
How to get a masked MSISDN:
Method 1 — Mobile-Initiated Request (recommended):
- Subscriber texts your shortcode → sourceAddress in callback = masked MSISDN
- Subscriber dials your USSD code → sourceAddress in callback = masked MSISDN
- Use https://echo.idmrt.dev/ to capture without running your own server
Method 2 — OTP Flow (for web/app users):
1. Call OTP Request with plain MSISDN → get referenceNo
2. Subscriber receives PIN via SMS
3. Call OTP Verify with referenceNo + PIN → get masked subscriberId
Key rules:
- You CANNOT construct a masked number — must receive from platform
- Each masked number is unique per-app (same subscriber = different hash per app)
- The masked MSISDN is stable — same subscriber always maps to same hash
- Store masked numbers in your database once obtained
MSISDN Format
- Correct:
tel:94771234567(no +, no spaces) - Masked:
tel:hu3b84346f...(pass as-is, treat as opaque) - Broadcast:
tel:all(entire subscriber base — guard carefully)
Operators (Sri Lanka)
| Operator | Prefixes |
|---|---|
| Dialog | 070, 076, 077 |
| Hutch | 072, 078 |
| Airtel | 075 |
SMS Send
Each API call shows Minimum (required only) and Comprehensive (all fields).
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"message": "Hello",
"destinationAddresses": ["tel:94771234567"]
}
Comprehensive:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"version": "1.0",
"message": "Hello world",
"destinationAddresses": ["tel:94771234567", "tel:94772345678"],
"sourceAddress": "tel:77123",
"deliveryStatusRequest": "1",
"encoding": "240",
"chargingAmount": "5.00",
"binaryHeader": "0605040B8423F0"
}
Fields:
| Field | Required | Description |
|---|---|---|
| applicationId | Yes | App ID |
| password | Yes | App password |
| message | Yes | Message body |
| destinationAddresses | Yes | ARRAY of tel: addresses. tel:all = broadcast |
| version | No | API version, defaults to latest |
| sourceAddress | No | Sender shown to user. Must be provisioned alias (E1331 if wrong) |
| deliveryStatusRequest | No | "0"=none (default), "1"=request DLR callback |
| encoding | No | "0"=Text, "240"=Flash, "245"=Binary (hex-encoded) |
| chargingAmount | No | Variable-charge amount per SMS (string, LKR) |
| binaryHeader | No | Hex UDH for binary messages (only with encoding "245") |
Rules:
destinationAddressesis ALWAYS an array, even for one recipient- Multi-recipient sends can partially succeed — check each
destinationResponse - 160 chars GSM-7, 70 chars Sinhala/Tamil (UCS-2) per segment
SMS Receive (MO Callback)
When a subscriber texts your shortcode:
{
"message": "SUBSCRIBE",
"sourceAddress": "tel:hu3b84346f63899a...",
"applicationId": "APP_XXXXXX",
"requestId": "22607072011552911",
"encoding": "0",
"version": "1.0"
}
sourceAddress= masked MSISDN — store this for future API calls- Respond:
{"statusCode": "S1000", "statusDetail": "Success"} - Recognise STOP/UNSUB/OFF → call Unregister
- Deduplicate on:
requestId
Use ideamart_sms_receive_parse tool to parse these payloads.
SMS Delivery Report (DLR Callback)
Fires after sending with deliveryStatusRequest: "1":
{
"destinationAddress": "tel:...",
"timeStamp": "20120113082110",
"requestId": "MSG_000111",
"deliveryStatus": "DELIVERED"
}
Status values (accept BOTH forms): - Long: DELIVERED, EXPIRED, DELETED, UNDELIVERABLE, ACCEPTED, UNKNOWN, REJECTED - Short (SMPP): DELIVRD, UNDELIV, ACCEPTD, REJECTD
ACCEPTED ≠ DELIVERED. Reports can arrive out of order, late, twice, or never.
USSD Send
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"message": "1. Balance\n2. Help",
"sessionId": "1330929317043",
"ussdOperation": "mt-cont",
"destinationAddress": "tel:94771234567"
}
Comprehensive:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"version": "1.0",
"message": "1. Balance\n2. Help\n3. Exit",
"sessionId": "1330929317043",
"ussdOperation": "mt-cont",
"destinationAddress": "tel:94771234567",
"encoding": "440"
}
Operations:
mt-init— App starts a new session to subscribermt-cont— Continue session (show menu, wait for input)mt-fin— End session (final message, close)
Rules:
- ECHO the platform's
sessionId— never generate your own - Terminal screens MUST use
mt-finor session hangs - Plain ASCII only (encoding 440) — no emoji, no Sinhala/Tamil
- Reply FAST — sessions timeout in seconds
- Session state: Redis with ~2 min TTL (NOT in-process Map)
USSD Receive (Callback)
{
"message": "*789#",
"sessionId": "1330929317043",
"sourceAddress": "tel:hu3b84346f...",
"applicationId": "APP_XXXXXX",
"ussdOperation": "mo-init",
"vlrAddress": "tel:...",
"encoding": "440",
"requestId": "1330933229901",
"version": "1.0"
}
mo-init= user just dialed your codemo-cont= user replied to menu- Respond with S1000, then send screen via
/ussd/send
Use ideamart_ussd_receive_parse tool to parse.
Subscription
Register (opt-in)
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567",
"action": "1"
}
Response includes subscriptionStatus: REGISTERED, UNREGISTERED, PENDING, or PENDING CHARGE.
- E1351 (already registered) = SUCCESS — desired state holds
- PENDING = not active yet, wait for notification
- Only call with explicit recorded consent
Unregister (opt-out)
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567",
"action": "0"
}
- E1356 (not registered) = SUCCESS — desired state holds
- Must be as easy as register (STOP/UNSUB/OFF keywords)
Check Status
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567"
}
Use for reconciliation, not per-request gate. Mirror state from notification callback.
Query Base
{
"applicationId": "APP_XXXXXX",
"password": "your-password"
}
Returns baseSize (string — parse before arithmetic). Best smoke test — no subscriber needed.
Subscription Notification (Callback)
{
"applicationId": "APP_XXXXXX",
"subscriberId": "tel:hu3b84346f...",
"status": "REGISTERED",
"frequency": "Monthly",
"version": "1.0",
"timeStamp": "20260811120000"
}
AUTHORITATIVE source of subscription state. Mirror locally.
OTP (One-Time PIN)
Use when subscriber interacts via web/app (not SMS/USSD) and you need their masked MSISDN.
OTP Request
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567"
}
Comprehensive:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567",
"applicationHash": "y3b84346f63899a",
"applicationMetaData": {
"client": "MOBILEAPP",
"device": "Samsung S10",
"os": "android8",
"appCode": "https://play.google.com/store/apps/details?id=lk.example.app"
}
}
Returns referenceNo — keep server-side only, never send to client.
OTP Verify
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"referenceNo": "3b84346f63899a32ec742a676532ec74dffe4f5",
"otp": "123456"
}
Returns subscriberId: "tel:hu3b84346f..." — the masked MSISDN. Store for all future API calls.
OTP Rules:
- Rate-limit per number AND per IP (prevents SMS-bombing)
- OTP valid 60 minutes
- Max 3 attempts per OTP
- Never log OTP or referenceNo
- E1850=Invalid, E1851=Expired, E1852=Max attempts, E1853=No active OTP
CaaS (Carrier Billing / Charging)
Direct Debit
⚠️ MOVES REAL MONEY
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"externalTrxId": "unique-id-max-32-chars",
"subscriberId": "tel:94771234567",
"amount": "50.00"
}
Comprehensive:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"externalTrxId": "unique-id-max-32-chars",
"subscriberId": "tel:94771234567",
"amount": "50.00",
"currency": "LKR",
"paymentInstrument": "MobileAccount",
"accountId": "12345"
}
Critical charging rules:
- Persist
externalTrxIdBEFORE the HTTP call — it's the idempotency key - NEVER retry with a fresh
externalTrxId(double-charge) - E1379 = already completed = SUCCESS (don't charge again)
- E1406 = user rejected = NEVER retry
- E1378 = insufficient balance — tell user, retry later
- Amount from server config, NEVER from client input
- Use decimal type, never float
Balance Query
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567"
}
Returns chargeableBalance (string — parse as decimal). Advisory only.
Charging Notification (Callback)
{
"externalTrxId": "your-id",
"internalTrxId": "PAY_00019283",
"subscriberId": "tel:94771234567",
"amount": "6.00",
"currency": "LKR",
"statusCode": "S1000",
"statusDetail": "Success",
"timeStamp": "2026-03-14T09:21:44.000+0530"
}
Reconcile by externalTrxId. Deduplicate on externalTrxId + statusCode.
LBS (Location)
Endpoint: https://api.dialog.lk/lbs/locate (different host!)
Minimum:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567",
"serviceType": "IMMEDIATE"
}
Comprehensive:
{
"applicationId": "APP_XXXXXX",
"password": "your-password",
"subscriberId": "tel:94771234567",
"serviceType": "IMMEDIATE",
"version": "2.0",
"responseTime": "NO_DELAY",
"horizontalAccuracy": "100",
"freshness": "HIGH_LOW"
}
Rules:
- Uses
api.dialog.lk, NOTapi.ideamart.io - Omit QoS fields unless you know your provisioned level (E1367 if too high)
- Coordinates are strings, ABSENT on failure
- Sanity-check: Sri Lanka lat ~6-10, lon ~79-82
- Requires explicit location consent (SMS consent ≠ location consent)
Webhook Testing with Echo2
Echo2 (https://echo.idmrt.dev/) captures MO requests without running your own server.
Capture SMS MO Request:
- Create Echo2 session — Go to https://echo.idmrt.dev/ → Create Session → get unique callback URL
- Set in Ideamart Portal — Your App → Settings → SMS Callback URL → paste Echo2 URL
- Trigger — Text your keyword to your short code from a real phone
- View on Echo2 — Payload appears in real-time:
{ "message": "HELLO", "sourceAddress": "tel:hu3b84346f63899a...", "applicationId": "APP_XXXXXX", "requestId": "22607072011552911", "encoding": "0", "version": "1.0" } - Extract —
sourceAddressis your masked MSISDN. Use it for all API calls.
Capture USSD MO Request:
- Set Echo2 URL as USSD Callback URL in portal
- Dial your USSD code (e.g.
*789#) from a phone - Echo2 captures:
{ "message": "*789#", "sessionId": "1330929317043", "sourceAddress": "tel:hu3b84346f...", "ussdOperation": "mo-init", "applicationId": "APP_XXXXXX" }
Capture Subscription Notification:
- Set Echo2 URL as Subscription Notification URL in portal
- When subscriber registers, Echo2 captures:
{ "subscriberId": "tel:hu3b84346f...", "status": "REGISTERED", "frequency": "Monthly" }
Echo2 Notes:
- Echo2 does NOT auto-respond S1000 — USSD sessions will timeout (use for testing only)
- Region-restricted — use session code from colleague if blocked
- After capturing masked MSISDN, switch callback back to your real server
- Works for all callback types: SMS MO, DLR, USSD, Subscription, Charging
Callbacks Summary
| Callback | Dedupe Key | What You Get |
|---|---|---|
| SMS MO | requestId | Masked MSISDN + message |
| SMS DLR | requestId + deliveryStatus | Delivery outcome |
| USSD Receive | requestId | Masked MSISDN + sessionId + user input |
| Subscription | subscriberId + status + timeStamp | State changes |
| Charging | externalTrxId + statusCode | Charge reconciliation |
All callbacks: Respond {"statusCode":"S1000"} immediately → process async → idempotent → validate applicationId.
Status Codes
Success & Benign:
| Code | Meaning |
|---|---|
| S1000 | Success |
| E1351 | Already registered (treat as SUCCESS for subscribe) |
| E1356 | Not registered (treat as SUCCESS for unsubscribe) |
| E1379 | Already completed (treat as SUCCESS for debit) |
Configuration (fix portal):
| Code | Meaning |
|---|---|
| E1303 | IP not whitelisted |
| E1309 | Service not provisioned |
| E1313 | Auth failure (wrong credentials or inactive) |
| E1331 | Source address not provisioned |
User-state (don't retry in loop):
| Code | Meaning |
|---|---|
| E1343 | Not whitelisted (Limited Production) |
| E1378 | Insufficient balance |
| E1405 | Charge authorization timed out |
| E1406 | User rejected charge (NEVER retry) |
OTP:
| Code | Meaning |
|---|---|
| E1850 | Invalid OTP (max 3 attempts) |
| E1851 | OTP expired (60 min) |
| E1852 | Max attempts reached |
| E1853 | No active OTP for reference |
Important: Ideamart returns HTTP 200 for ALL responses. Always check
statusCodein the body.
Common Mistakes
| Mistake | Consequence |
|---|---|
if (response.ok) return "sent" |
Every error reported as success |
destinationAddresses: "tel:94..." |
Must be ARRAY — fails silently |
| Hardcoded credentials | In git, can charge subscribers |
| Retry debit with new externalTrxId | Double-charges a real person |
| Self-generated USSD sessionId | Sessions orphan, screen blank |
| USSD state in in-process Map | Breaks on scale — use Redis |
| Process before callback ack | Timeouts, duplicates |
rejectUnauthorized: false |
Credentials interceptable |
| E1351/E1356/E1379 as failure | Working flows reported broken |
IP Whitelisting
Every Ideamart app requires whitelisting the calling server's IP:
- MCP Server IP: 54.255.44.189
- Check your IP: Use ideamart_ip_check tool or curl -4 https://myip.ideamart.io
- Add in: Ideamart Portal → Your App → Settings → Allowed Host Addresses
- Without it: all calls fail with E1303
Environment Variables Pattern
IDEAMART_APP_ID=APP_XXXXXX
IDEAMART_PASSWORD=replace-me
# Uncomment only what is provisioned:
#IDEAMART_SMS_SEND_URL=https://api.ideamart.io/sms/send
#IDEAMART_USSD_SEND_URL=https://api.ideamart.io/ussd/send
#IDEAMART_SUBSCRIPTION_SEND_URL=https://api.ideamart.io/subscription/send
#IDEAMART_CAAS_DEBIT_URL=https://api.ideamart.io/caas/direct/debit
#IDEAMART_LBS_URL=https://api.dialog.lk/lbs/locate
#IDEAMART_OTP_REQUEST_URL=https://api.ideamart.io/subscription/otp/request
#IDEAMART_OTP_VERIFY_URL=https://api.ideamart.io/subscription/otp/verify
An unset URL means that API is not enabled — refuse locally rather than getting E1309.
Quick Smoke Test
curl -sS -X POST https://api.ideamart.io/subscription/query-base \
-H 'Content-Type: application/json' \
-d '{"applicationId":"APP_XXXXXX","password":"your-password"}'
If you get S1000 with baseSize, credentials and IP are correct.
Retry Discipline
- Retry ONLY transient codes (E1316, E1332, E1341, E1600-E1603) and transport errors
- Exponential backoff: base 1s, factor 2, jitter ±30%, max 5 attempts
- After max → dead-letter queue for manual review
- NEVER retry configuration errors
- NEVER retry user-state errors in tight loop
- NEVER retry debit with new externalTrxId
Tips
- Provide
application_idandpasswordonce per session — the AI remembers them - Parsing tools (
sms_receive_parse,ussd_receive_parse) are offline — no API calls - Use
ideamart_docstool with topic filter for quick reference (topics: sms, otp, charging, masking, status, rules, mistakes, retry) - Use
ideamart_ip_checkto verify the MCP server's outgoing IP - The AI can chain workflows (parse SMS → check subscription → send reply → charge)
Support
- Portal: https://portal.ideamart.io
- Docs: https://docs.ideamart.io
- Email: info@ideamart.io
- WhatsApp: +94767412345
- Always quote
requestId/externalTrxId/sessionId+statusCodewhen contacting support