SMS Webhook API

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).

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 sent
  • success — 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) or all
  • [ ] Create a POST endpoint in your app to receive webhooks
  • [ ] Add authentication (API key header or similar)
  • [ ] Handle deduplication using smsId
  • [ ] Return 2xx on 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: smsspike on localhost:3306
  • SMS storage table: ShopOnSmsInbox
  • Hooks table: ShopOnSmsHooks
  • Deliveries table: ShopOnSmsDeliveries
  • Processing: Cron job runs smsMonitor.js every 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.