MsgHub REST API
Programmatic access to MsgHub through one REST API. Auth with a Bearer API key, send template messages on any channel, manage contacts, track events, and trigger campaigns. JSON over HTTPS. One unified POST /messages/send endpoint — the template's channel determines whether the message is dispatched as SMS, WhatsApp, Email or RCS.
Quick start
Send your first message in 60 seconds. Two steps: create the contact, then send a message referencing one of your approved templates. Replace YOUR_API_KEY with a key generated from the API Keys page in the sidebar, and TEMPLATE_UUID with a template ID from Templates.
# 1. Upsert the contact curl https://app.msghub.info/api/v1/contacts \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone": "+919876543210", "name": "Riya" }' # 2. Send a message using an approved template curl https://app.msghub.info/api/v1/messages/send \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": { "phone": "+919876543210" }, "templateId": "TEMPLATE_UUID" }' # Response (201 Created) { "success": true, "data": { "messageId": "" , "status": "queued" } }
Authentication
Every request must include a Bearer API key in the Authorization header:
Authorization: Bearer mh_live_xxxxxxxxxxxxxxxxxxxxxxxx
API keys are scoped to a single tenant. To create one: sidebar → API Keys → "+ New API Key". Pick a name and tick the scopes you need. Keys are shown once at creation — copy and store securely.
Never ship API keys in front-end code. Anyone with the key can send messages from your account. Keep server-side only. If a key leaks, revoke from the API Keys page.
Scopes
Pick the minimum set when creating a key:
| Scope | What it allows |
|---|---|
contacts:write | Create / update contacts |
contacts:read | Read contacts |
messages:send | Send messages via approved templates |
campaigns:trigger | Trigger a draft campaign |
events:write | Track custom events for a contact |
templates:read | List approved templates |
Base URL & versioning
https://app.msghub.info/api/v1
The dashboard and API share the same domain. All public endpoints live under the /api/v1 prefix. Future breaking changes ship as /api/v2.
Request format
- Method: standard REST verbs — GET, POST.
- Body: JSON.
Content-Type: application/jsonrequired on POST. - Phone numbers: include country code with leading
+— e.g.+919876543210. Stored as-is per tenant. - IDs: UUIDs (templates, campaigns, messages). Treat as opaque strings.
- CORS: all
/api/v1/*responses includeAccess-Control-Allow-Origin: *so the API can be called cross-origin from browser SDKs and widget integrations.
Response format
Every response uses a consistent envelope: { success: true, data: … } on the happy path, { success: false, error: { code, message } } on failure.
// Successful POST → 201 Created { "success": true, "data": { "id": "" , "created": true } } // List GET → 200 OK { "success": true, "data": [ /* rows */ ] } // Failure { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "…" } }
Error codes
All errors return the failure envelope above. Common codes you'll encounter:
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body failed schema validation |
| 401 | UNAUTHENTICATED | Missing or invalid API key (Authorization: Bearer … required) |
| 403 | FORBIDDEN | API key lacks the required scope |
| 404 | NOT_FOUND / CONTACT_NOT_FOUND | Template or contact missing (create contact first) |
| 422 | TEMPLATE_NOT_APPROVED | Template exists but isn't approved yet |
| 422 | CONTACT_BLOCKED | Contact is inactive or blocked |
| 422 | CONTACT_DND | Contact opted out of this channel |
| 422 | NO_EMAIL | Email template requires a contact with an email address |
| 429 | RATE_LIMITED | Too many requests — slow down |
| 500 | INTERNAL_ERROR | Server error — safe to retry after backoff |
Rate limits
- Global cap: 300 requests/minute per source IP across all endpoints.
- Researcher-tier tenants (vulnerability-disclosure participants): 60 req/minute per API key, enforced via Redis counter.
- Webhook receivers: 1000/minute (handled by a separate webhook scope).
Exceeding the cap returns:
HTTP 429 Too Many Requests { "success": false, "error": { "code": "RATE_LIMITED", "message": "Too many requests. Please slow down." } }
Honour the response and back off. Higher limits available on request for enterprise plans.
Pagination
List endpoints (e.g. GET /contacts) use simple page + limit query parameters:
GET /api/v1/contacts?page=1&limit=50 # Response { "success": true, "data": [ /* up to `limit` rows */ ] }
page is 1-indexed. limit defaults to 50, capped at 100. Increment page until you receive an empty array.
Endpoints
Six public endpoints under /api/v1. There is no per-channel send endpoint — one unified /messages/send with a templateId handles every channel. Conversation listing and template submission are dashboard-only for now.
Send a message
Body parameters
to | object | required | { phone?, email? } — at least one. Contact must exist in this tenant. |
templateId | UUID | required | UUID of an approved template. The template's channel (sms / whatsapp / email / rcs) determines the dispatch route. |
Required scope
messages:send
Example request
POST /api/v1/messages/send Authorization: Bearer mh_live_... Content-Type: application/json { "to": { "phone": "+919876543210" }, "templateId": "e2f8b1a0-…-uuid" }
Response 201
{
"success": true,
"data": {
"messageId": "" ,
"status": "queued"
}
}
Failure modes
NOT_FOUND— template doesn't existTEMPLATE_NOT_APPROVED— template still pending/rejectedCONTACT_NOT_FOUND— create contact first viaPOST /api/v1/contactsCONTACT_BLOCKED,CONTACT_DND,NO_EMAIL— contact-state issues
To send DLT-compliant SMS, the underlying template must be approved on your DLT portal and registered against your SMS provider — see DLT SMS. The API behaves the same regardless; only the template metadata differs.
Contacts
Body parameters
phone | string | — | Phone with country code, e.g. +919876543210 |
email | string | — | Email address |
name | string | — | Display name |
tags | string[] | — | Tags to add (merged with any existing) |
meta | object | — | Custom fields, merged into the contact's customFields |
At least one of phone or email is required. Upsert behaviour: if a contact with the same phone (or email) exists, it's updated and the response returns { created: false }.
Submitting an identifier is treated as consent for the channels provided (smsConsent + waOptIn for phone, emailConsent for email).
Required scope: contacts:write
Response
// 201 Created (new) or 200 OK (updated) { "success": true, "data": { "id": "" , "created": true } }
Mark a contact as DND (do-not-disturb). Body: { phone? , email? } — at least one. Matching contacts are flagged isDnd: true and excluded from non-email sends.
Required scope: contacts:write
Query parameters
page | integer | — | 1-indexed page number (default 1) |
limit | integer | — | Per-page count (default 50, max 100) |
Required scope: contacts:read
Returns { success: true, data: [ { id, phone, email, name, isDnd, isActive, tags, source, createdAt }, … ] }.
Events
Body parameters
event | string | required | Event name (max 100 chars), e.g. cart.abandoned |
contact | object | required | { phone?, email? } identifying the contact |
Custom events feed into automation triggers. Use them to fire flows on real business events: order completion, cart abandonment, NPS submission, etc.
Required scope: events:write
Trigger a campaign
Launch a campaign that's currently in draft state. Body: { campaignId: UUID }. The campaign is queued for dispatch and its state moves to processing.
Required scope: campaigns:trigger
Returns 422 INVALID_STATE if the campaign isn't a draft.
Templates
Query parameters
channel | string | — | Filter by channel: sms · whatsapp · email · rcs |
Returns only approved templates. Submit new templates for approval from the dashboard Templates page — the API doesn't currently expose template creation.
Required scope: templates:read
Response shape:
{
"success": true,
"data": [
{ "id": "" , "name": "order_shipped", "channel": "whatsapp", "smsBody": null }
]
}
Webhooks
MsgHub can push real-time events to your server — delivery receipts, contact unsubscribes, campaign completion. Add endpoints from the Webhooks page in the sidebar (top-level item, not nested under Settings).
Webhooks are signed with HMAC-SHA256. The signature header uses a Stripe-style t={timestamp},v1={signature} format. Verify on every incoming request.
SDKs & examples
A JavaScript SDK is available at https://app.msghub.info/api/v1/sdk.js — drop one script tag and call MsgHub.identify(), track(), send(). For server-side, use any HTTP client.
Browser SDK (no install)
<!-- in your HTML --> <script> window.MsgHub = { apiUrl: 'https://app.msghub.info', apiKey: 'mh_live_…' // public-scope only — never an admin key }; </script> <script src="https://app.msghub.info/api/v1/sdk.js" async></script> <script> // Identify the visitor (creates / updates the contact) MsgHub.identify({ phone: '+919876543210', email: '[email protected]', name: 'Ravi' }); // Track a custom event MsgHub.track('cart.abandoned', { phone: '+919876543210' }); </script>
Node.js (vanilla)
const base = 'https://app.msghub.info/api/v1'; const headers = { 'Authorization': `Bearer ${process.env.MSGHUB_KEY}`, 'Content-Type': 'application/json', }; // 1. Upsert the contact await fetch(`${base}/contacts`, { method: 'POST', headers, body: JSON.stringify({ phone: customer.phone, name: customer.name }), }); // 2. Send an approved template const res = await fetch(`${base}/messages/send`, { method: 'POST', headers, body: JSON.stringify({ to: { phone: customer.phone }, templateId: 'e2f8b1a0-…-uuid', }), }); const data = await res.json(); if (!data.success) throw new Error(data.error.message);
Python (requests)
import requests, os base = "https://app.msghub.info/api/v1" headers = { "Authorization": f"Bearer {os.environ['MSGHUB_KEY']}" } # Upsert contact requests.post(f"{base}/contacts", headers=headers, json={"phone": customer.phone, "name": customer.name}) # Send approved template resp = requests.post(f"{base}/messages/send", headers=headers, json={ "to": {"phone": customer.phone}, "templateId": template_id }) resp.raise_for_status() data = resp.json() if not data["success"]: raise Exception(data["error"]["message"])
PHP (cURL)
$base = 'https://app.msghub.info/api/v1'; $headers = [ 'Authorization: Bearer ' . getenv('MSGHUB_KEY'), 'Content-Type: application/json', ]; // Send approved template $ch = curl_init($base . '/messages/send'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode([ 'to' => ['phone' => $customer->phone], 'templateId' => $templateId, ]), ]); $data = json_decode(curl_exec($ch), true);
Other integrations
For point-and-click integration without writing code:
- Zapier — sidebar → Integrations → Zapier tab. Triggers (new message, new contact, message status) and Actions (send SMS, send WhatsApp, add contact, trigger flow).
- Shopify — sidebar → Integrations → Shopify. Embedded signup, then per-event automations.
- Zoho CRM / HubSpot — sidebar → Integrations → CRM tab. OAuth2 flow + contact sync.
- Odoo — sidebar → Integrations → Odoo ERP. Provides a webhook URL + Python snippet for the Odoo side.
FAQ
How do I get an API key?
In MsgHub, click API Keys in the left sidebar (it's a top-level item — not nested under Settings). Click + New API Key, choose a name and the scopes you need, then click Create. The key is shown once — copy it immediately. Keys use the format mh_live_<hex>.
What's the API base URL?
https://app.msghub.info/api/v1. The dashboard and API share the same hostname; the API lives under /api/v1.
Is there a JavaScript SDK?
Yes — drop https://app.msghub.info/api/v1/sdk.js in your page, set window.MsgHub = { apiUrl, apiKey }, and call MsgHub.identify(), track(), or send(). Node, Python and PHP wrappers aren't yet packaged; use any HTTP client meanwhile.
What are the rate limits?
300 requests/minute per source IP globally. Researcher-tier tenants are tighter at 60 req/min per API key. Hitting the limit returns 429 with error.code = "RATE_LIMITED" — slow down and retry.
Is there an Idempotency-Key header?
Not currently. Each POST /messages/send creates a separate message record. If your application retries on failure, dedupe on your side (e.g. by the response messageId the first call returned). Idempotency-key support is on the roadmap.
Can I send via multiple channels in one request?
One template = one channel. Choose the template whose channel matches what you want to send. If you want a fallback (try WhatsApp, fall back to SMS), implement it client-side: call /messages/send for WhatsApp first, listen to message.failed webhook, then call again with the SMS template.
Why does my send return CONTACT_NOT_FOUND?
The contact must exist in your tenant before you can send to it. Create or upsert the contact via POST /api/v1/contacts first (with at least a phone or email). Then POST /messages/send with the same identifier.
Why does my send return TEMPLATE_NOT_APPROVED?
WhatsApp templates need Meta approval (1–2 hours typically). DLT SMS templates need DLT-portal approval. Only templates in approved state are dispatchable. List approved templates with GET /api/v1/templates.
Are API keys per-tenant or per-user?
Per-tenant. Keys aren't tied to a user account, so they keep working when team members leave. Every API call is logged with the key that made it for audit purposes.