SMS Gateway Webhook System — Developer Spec
Version: 1.0
Date: April 18, 2026
Status: Production-ready
Base URL: https://apps.storeharmony.com/boss/api
Overview
Storeharmony operates a centralized SMS gateway that receives all incoming SMS via an Android device running Android SMS Gateway. Messages are stored, classified, and routed to registered applications via webhooks.
Your app can subscribe to receive SMS that match specific criteria. The gateway handles classification, matching, delivery, and retries — your app just needs an HTTPS endpoint that accepts POST requests.
How It Works
Android Device (SIM)
│
▼
SMS Gateway Webhook (/api/sms/incoming)
│
▼
ShopOnSmsInbox (stored + classified)
│
▼
Hook Matching Engine (runs every 60s)
│
├── Hook: "Cashwise - bank alerts" → POST https://your-app/api/sms/incoming
├── Hook: "iLeap - vote SMS" → POST https://your-app/api/sms/vote
└── Hook: "MyApp - all SMS" → POST https://your-app/api/sms/incoming
SMS Classification
Every incoming SMS is automatically classified into one of these categories:
| Category | Description | Examples |
|---|---|---|
bank_alert |
Nigerian bank credit/debit alerts | PROVIDUS, GTBANK, ZENITH, etc. |
otp |
One-time passwords, verification | “Your OTP is 123456” |
promo |
Promotional/marketing messages | OPAY, PALMPAY offers |
merchant |
Messages from known phone numbers | Customer replies, vendor messages |
unknown |
Everything else | Unrecognized senders |
For bank_alert messages, the system also extracts:
amount(float)currency(e.g., “NGN”)transactionType(“credit” or “debit”)account(masked account number if found)balance(if found)
Registering a Webhook (Hook)
Create Hook
POST /admin/sms/hooks
Header: X-Admin-Key: <admin-key>
Content-Type: application/json
Request Body:
{
"name": "My App - bank alerts",
"filterType": "sender",
"filterValue": "PROVIDUS",
"webhookUrl": "https://your-app.example.com/api/sms/incoming",
"webhookMethod": "POST",
"webhookHeaders": {
"X-API-Key": "your-secret-key"
},
"webhookBodyTemplate": null,
"isActive": true
}
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ✅ | Human-readable name for this hook |
filterType |
enum | ✅ | One of: sender, category, amount, all |
filterValue |
string | ✅ | The value to match (ignored when filterType is all) |
webhookUrl |
string | ✅ | HTTPS URL to receive the webhook POST |
webhookMethod |
enum | No | POST (default) or GET |
webhookHeaders |
object | No | Custom headers sent with the webhook request |
webhookBodyTemplate |
object | No | Custom payload template (see Payload Templates below) |
isActive |
boolean | No | Default true. Set false to pause without deleting. |
Filter Types
| filterType | filterValue examples | Matching logic |
|---|---|---|
sender |
"PROVIDUS", "08035" |
Case-insensitive partial match on sender name |
category |
"bank_alert", "otp" |
Exact match on classified category |
amount |
">5000", "<=100000" |
Numeric comparison on parsed amount |
all |
"*" (any value) |
Matches every SMS — app does its own filtering |
Amount filter operators: >, >=, <, <=, =
Examples
Receive all bank alerts:
{
"name": "Cashwise - all bank alerts",
"filterType": "category",
"filterValue": "bank_alert",
"webhookUrl": "https://apps.storeharmony.com/cashwise/api/sms/incoming"
}
Receive only PROVIDUS alerts over ₦10,000:
{
"name": "Audit - large PROVIDUS txns",
"filterType": "amount",
"filterValue": ">10000",
"webhookUrl": "https://apps.storeharmony.com/audit/api/alerts"
}
Receive ALL SMS (app does its own matching):
{
"name": "iLeap - all SMS",
"filterType": "all",
"filterValue": "*",
"webhookUrl": "https://apps.storeharmony.com/ileap/api/sms/incoming"
}
Webhook Payload
When a match is found, the gateway POSTs a JSON payload to your webhookUrl.
Default Payload
{
"smsId": 153,
"hookId": 1,
"hookName": "My App - bank alerts",
"sender": "PROVIDUS",
"message": "Credit Alert: NGN5,000.00 FROM TEST SENDER 13******93 on 18/04/2026 @ 15:45:30 Bal: NGN100,000.00",
"category": "bank_alert",
"parsed": {
"amount": 5000.0,
"currency": "NGN",
"category": "bank_alert",
"transactionType": "credit",
"account": null,
"balance": null,
"description": null
},
"timestamp": "2026-04-18T20:11:02.000Z"
}
Custom Payload Templates
If you set webhookBodyTemplate, the gateway uses it as a template with variable substitution:
{
"webhookBodyTemplate": {
"id": "{{smsId}}",
"from": "{{sender}}",
"text": "{{message}}",
"type": "{{category}}",
"value": "{{amount}}"
}
}
Available variables: {{smsId}}, {{hookId}}, {{sender}}, {{message}}, {{category}}, {{amount}}
What Your App Needs to Implement
1. Webhook Endpoint
Create a POST endpoint that accepts the webhook payload:
POST /api/sms/incoming
Content-Type: application/json
2. Response
Return any 2xx status code to acknowledge receipt:
{ "ok": true }
Important: If your endpoint returns a non-2xx status, the gateway will retry (see Delivery & Retries).
3. Validation (Recommended)
If you set custom headers (e.g., X-API-Key), validate them in your endpoint:
app.post('/api/sms/incoming', (req, res) => {
if (req.headers['x-api-key'] !== process.env.SMS_HOOK_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { smsId, sender, message, category, parsed } = req.body;
// Your logic here
// e.g., create an expense from a bank alert
// e.g., tally a vote from a keyword SMS
res.json({ ok: true });
});
4. Idempotency
The gateway may deliver the same SMS more than once (retries, duplicate matching). Use smsId as a deduplication key:
// Check if already processed
const existing = await db.query('SELECT id FROM processed_sms WHERE smsId = ?', [req.body.smsId]);
if (existing.length > 0) {
return res.json({ ok: true, duplicate: true });
}
Delivery & Retries
| Setting | Value |
|---|---|
| Timeout | 10 seconds per attempt |
| Max attempts | 5 |
| Retry strategy | Exponential backoff |
| Retry intervals | ~1min, ~2min, ~4min, ~8min, ~16min |
| Processing cadence | Every 60 seconds (cron) |
Delivery statuses:
pending— Waiting to be sentsuccess— Delivered (2xx response)failed— All retry attempts exhausted or permanent error
Managing Hooks
All endpoints require X-Admin-Key header.
| Method | Endpoint | Description |
|---|---|---|
GET |
/admin/sms/hooks |
List all hooks |
POST |
/admin/sms/hooks |
Create a new hook |
PUT |
/admin/sms/hooks/:id |
Update a hook |
DELETE |
/admin/sms/hooks/:id |
Deactivate a hook |
GET |
/admin/sms/hooks/:id/deliveries |
View delivery history |
List Hooks
curl -H "X-Admin-Key: $KEY" https://apps.storeharmony.com/boss/api/admin/sms/hooks
Delete (Deactivate) Hook
curl -X DELETE -H "X-Admin-Key: $KEY" https://apps.storeharmony.com/boss/api/admin/sms/hooks/1
View Deliveries
curl -H "X-Admin-Key: $KEY" https://apps.storeharmony.com/boss/api/admin/sms/hooks/1/deliveries
Integration Checklist
- [ ] Decide your filter strategy: specific (
sender/category/amount) orall - [ ] Create a
POSTendpoint in your app to receive webhooks - [ ] Add authentication (API key header or similar)
- [ ] Handle deduplication using
smsId - [ ] Return
2xxon success, non-2xx to trigger retry - [ ] Register your hook via the admin API
- [ ] Test with a real SMS (or ask Jane to insert a test record)
Architecture Notes
- Database:
smsspikeonlocalhost:3306 - SMS storage table:
ShopOnSmsInbox - Hooks table:
ShopOnSmsHooks - Deliveries table:
ShopOnSmsDeliveries - Processing: Cron job runs
smsMonitor.jsevery 60 seconds - Webhook timeout: 10s per request
- All apps on same server: Webhook calls are localhost → near-zero latency
Example: Cashwise Bank Alert Integration
// cashwise/server.js — add this endpoint
app.post('/api/sms/incoming', async (req, res) => {
const { smsId, sender, message, category, parsed } = req.body;
// Only process bank alerts
if (category !== 'bank_alert' || !parsed?.amount) {
return res.json({ ok: true, skipped: true });
}
// Dedup check
const [existing] = await db.query(
'SELECT id FROM cw_sms_pool WHERE rawText = ? LIMIT 1', [message]
);
if (existing.length > 0) {
return res.json({ ok: true, duplicate: true });
}
// Create unassigned expense in SMS pool
await db.query(
`INSERT INTO cw_sms_pool (rawText, amount, expenseItem, status, createdByUserId)
VALUES (?, ?, ?, 'unassigned', 1)`,
[message, parsed.amount, `${parsed.transactionType} - ${sender}`]
);
res.json({ ok: true });
});
Then register the hook:
curl -X POST -H "Content-Type: application/json" -H "X-Admin-Key: $KEY"
-d '{
"name": "Cashwise - bank alerts",
"filterType": "category",
"filterValue": "bank_alert",
"webhookUrl": "https://apps.storeharmony.com/cashwise/api/sms/incoming",
"webhookHeaders": {"X-API-Key": "cashwise-sms-secret-2026"}
}'
https://apps.storeharmony.com/boss/api/admin/sms/hooks
Built by Jane for Storeharmony internal apps. Contact Deji for admin API key access.