MojaWave
Login Get Started →
Documentation
v1.0 · REST API

MojaWave API Reference

The MojaWave API lets you send SMS and transactional email — all from a single, unified REST interface.

Base URL: https://api.mojawave.com/v1
SMS API
Send single, bulk, and OTP messages across TZ networks.
5 endpoints →
Email API
Send transactional emails with custom domains, CC/BCC, attachments, and scheduling.
8 endpoints →
Webhooks
Real-time event delivery with HMAC-SHA256 verification.
4 event types →
Sandbox
Test your integration with no real transactions or charges.
Test numbers →

Quickstart

Make your first API call in under two minutes. All you need is an API key — grab one from the developer dashboard or use a sandbox key to start immediately.

# Send your first SMS
curl -X POST https://api.mojawave.com/v1/sms/send \
  -H "Authorization: Bearer mw_8472910" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+255753276939",
    "from": "MojaWave",
    "message": "Hello from Mojawave! Your verification code is 1234."
  }'
import mojawave

client = mojawave.Client(api_key="mw_8472910")
sms = client.sms.send(
    to="+255712345678",
    from_="MojaWave",
    message="Your OTP is 4821. Valid for 5 minutes."
)
print(sms.id, sms.status)  # 3fa85f64...  queued
import { MojaWave } from 'mojawave';

const client = new MojaWave({ apiKey: 'mw_8472910' });
const sms = await client.sms.send({
  to: '+255712345678',
  from: 'MojaWave',
  message: 'Your OTP is 4821. Valid for 5 minutes.',
});
console.log(sms.id, sms.status); // 3fa85f64...  queued

Response · 200 OK

JSON
{
  "success": true,
  "data": {
    "id": "89b82624-f1a2-4f5e-85b5-102e79a06779",
    "type": "sms",
    "to": "+255753276939",
    "status": "sent",
    "body": "Hello from Mojawave! Your verification code is 1234.",
    "segments": 1,
    "credits_cost": 1,
    "timeline": {
      "queued_at": "2026-04-05T12:03:04.485Z",
      "sent_at": "2026-04-05T12:04:04.393Z"
    }
  }
}

Authentication

MojaWave uses bearer token authentication. Pass your API key in the Authorization header of every request.

Never expose your API key in client-side code. Use environment variables and server-side requests only.
Authorization header
Authorization: Bearer mw_YOUR_KEY
API Key Types
PrefixEnvironmentUsage
mw_ Live Real transactions. Keep this secret.
sk_test_mw_ Sandbox No real charges. Safe for development.

Errors & Rate Limits

MojaWave uses standard HTTP status codes. Error responses always include a JSON body with a code and human-readable message.

HTTP StatusCodeMeaning
200Success
400invalid_requestMissing or malformed parameters
401unauthorizedInvalid or missing API key
402insufficient_balanceAccount balance too low
422unprocessableValidation failed on request body
429rate_limit_exceededToo many requests — back off and retry
500server_errorSomething went wrong on our end

Rate Limits

Default limits: 600 requests/min on the live environment and 120 requests/min on sandbox. Limits are per API key and vary by plan. Rate limit headers are included in every response:

  • X-RateLimit-Limit — your cap per minute
  • X-RateLimit-Remaining — requests left in current window
  • X-RateLimit-Reset — Unix timestamp when limit resets

SMS — Send Message

Send a single SMS to any Tanzania mobile number. Messages are delivered via direct telco connections to Vodacom, Tigo, Airtel, and Halotel.

POST /v1/sms/send
Request Body Parameters application/json
ParameterTypeRequiredDescription
to string required Recipient phone in E.164 format. e.g. +255712345678
from string required Sender ID (max 11 chars alphanumeric) or MojaWave.
message string required Message content. Long messages are split into segments automatically.
webhook_url string optional URL to receive delivery status webhooks for this message.
schedule_at string optional ISO 8601 timestamp for scheduled delivery.
metadata object optional Additional custom key-value pairs for your tracking.
Example Request Body
{
  "to": "+255753276939",
  "from": "MojaWave",
  "message": "Hello from Mojawave! Your verification code is 1234.",
  "webhook_url": "https://example.com/webhook/sms",
  "metadata": {
    "customer_id": "cust98765"
  },
  "tags": ["onboarding", "verification"]
}
201 CreatedMessage Sent
{
  "success": true,
  "data": {
    "id": "89b82624-f1a2-4f5e-85b5-102e79a06779",
    "type": "sms",
    "to": "+255753276939",
    "status": "sent",
    "segments": 1,
    "credits_cost": 1,
    "queued_at": "2026-04-05T12:03:04.485Z",
    "sent_at": "2026-04-05T12:04:04.393Z"
  }
}
Delivery receipts: Set a webhook_url to receive real-time delivery updates via POST requests.

SMS — Get Message Details

Retrieve full details of a message including delivery timeline and metadata.

GET /v1/messages/{message_id}
Response Schema (data object)
FieldTypeDescription
iduuidUnique identifier for the message.
statusstringCurrent status (queued, sent, delivered, failed).
credits_costfloatCredits used for this message.
timelineobjectDelivery checkpoints: queued_at, sent_at, delivered_at.
failure_reasonstringHuman-readable reason if delivery failed.
200 OKMessage Details
{
  "success": true,
          "data": {
    "id": "89b82624-f1a2-4f5e-85b5-102e79a06779",
    "type": "sms",
    "to": "+255753276939",
    "from": "MojaWave",
    "status": "delivered",
    "segments": 1,
    "credits_cost": 1,
    "timeline": {
      "queued_at": "2026-04-05T12:03:04.485Z",
      "sent_at": "2026-04-05T12:04:04.393Z",
      "delivered_at": "2026-04-05T12:05:06.751Z"
    }
  }
}

SMS — Bulk Send

Send the same message to multiple recipients in one API call. Up to 10,000 recipients per request. Bulk jobs are processed asynchronously — a job ID is returned immediately.

POST /v1/sms/bulk
Request body
{
  "name": "Marketing Campaign Q1",
  "from": "MojaWave",
  "message": "Hello , your code is ",
  "recipients": [
    {
      "to": "+255712345678",
      "personalization": {
        "name": "John",
        "code": "ABC123"
      }
    },
    {
      "to": "+255712345678",
      "personalization": {
        "name": "Jane",
        "code": "ABC123"
      }
    }
  ],
  "webhook_url": "https://example.com/webhooks"
}

Response · 202 Accepted

JSON
{
  "success": true,
  "data": {
    "job_id": "ec0fb57c-8b90-4e21-9f96-48235d6a05ac",
    "status": "scheduled",
    "total_recipients": 2,
    "estimated_credits": 2,
    "has_personalization": true,
    "personalization_fields": ["code", "name"],
    "scheduled_at": "2026-04-05T12:07:14.559Z",
    "created_at": "2026-04-05T12:06:25.317Z"
  }
}
Bulk sends using unicode type have a per-segment character limit of 70 vs 160 for plain SMS. Plan your message length accordingly.

SMS — Get Bulk Job Details

Retrieve the status, progress, and statistical summary of a bulk SMS job.

GET /v1/sms/bulk/{jobId}
FieldTypeDescription
statusstringqueued, processing, completed, failed
progress_percentfloatJob completion percentage (0-100).
sent_countintegerNumber of messages successfully sent.
total_credits_costfloatFinal total credits consumed by the job.
JSON Response
{
  "success": true,
  "data": {
    "id": "ec0fb57c-8b90-4e21-9f96-48235d6a05ac",
    "name": "Marketing Campaign Q1",
    "status": "completed",
    "total_recipients": 2,
    "sent_count": 2,
    "progress_percent": 100.0,
    "total_credits_cost": 2,
    "completed_at": "2026-04-05T12:08:09.610Z"
  }
}

SMS — Sender IDs

Retrieve the approved alphanumeric sender IDs on your account. Use the value of sender_id in the sender_id field when sending SMS. Only approved IDs can be used — this endpoint returns only those.

GET /v1/sms/sender-ids/approved List approved sender IDs

Returns only approved sender IDs — the ones ready to use when sending SMS. No status filtering needed; call this endpoint to discover what's available.

Query Parameters

ParameterTypeDescription
limitintegerMax results per page (1–100, default 50).
offsetintegerPagination offset (default 0).

Response Fields

FieldTypeDescription
sender_idstringThe alphanumeric name shown to recipients. Use this value when sending SMS.
statusstringAlways approved on this endpoint.
purposestringBusiness purpose provided at registration.
rejection_reasonstring|nullAlways null on this endpoint.
created_atdatetimeISO-8601 UTC timestamp of registration.
updated_atdatetimeISO-8601 UTC timestamp of last status change.
200 OK
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "sender_id": "MYAPP",
        "purpose": "Transactional notifications for MYAPP platform",
        "status": "approved",
        "rejection_reason": null,
        "created_at": "2026-03-10T08:22:14.000Z",
        "updated_at": "2026-03-12T11:05:00.000Z"
      }
    ],
    "total": 1,
    "limit": 50,
    "offset": 0
  }
}

Email — Domain Management

Register the domain you want to send from and add two DNS records at your provider. Once domain status becomes verified and you can register sender addresses on it.

Verification typically takes 1–48 hours depending on your DNS provider's TTL.
GET /v1/email/domains List all domains
FieldTypeDescription
iduuidDomain identifier — use in subsequent requests.
domain_namestringThe registered domain (e.g. example.com).
statusstringpending or verified. Only verified domains can send.
dkim_statusstringpending or verified — DKIM TXT record check.
cname_statusstringpending or verified — Bounce CNAME record check.
200 OKDomain List
{
  "success": true,
  "data": [
    {
      "id": "10e893ef-7503-483a-941c-f92ed7bb90f4",
      "domain_name": "yourcompany.com",
      "status": "verified",
      "dkim_status": "verified",
      "cname_status": "verified",
      "verified_at": "2026-06-13T16:18:30.302946Z",
      "created_at": "2026-06-13T11:54:43.427958Z"
    }
  ],
  "error": null,
  "meta": null
}
GET /v1/email/domains/{id} Domain detail + DNS records

Returns full domain detail including the exact DNS records to add at your provider.

DNS Records to Add
RecordTypeHost (example)Purpose
dkimTXT1345443._domainkey.yourdomain.comEmail authentication — prevents spoofing.
cnameCNAMEbounce.yourdomain.comBounce tracking subdomain.
200 OKDomain Detail
{
  "success": true,
  "data": {
    "id": "10e893ef-7503-483a-941c-f92ed7bb90f4",
    "domain_name": "yourcompany.com",
    "sub_domain_prefix": "bounce",
    "status": "verified",
    "dkim_status": "verified",
    "cname_status": "verified",
    "dns_records": {
      "dkim": {
        "type": "TXT",
        "host": "1345443._domainkey.yourcompany.com",
        "value": "k=rsa; p=MIGfMA0GCSqGSIb3DQEBA..."
      },
      "cname": {
        "type": "CNAME",
        "host": "bounce.yourcompany.com",
        "points_to": "cluster89.zeptomail.com"
      }
    },
    "verified_at": "2026-06-13T16:18:30.302946Z",
    "created_at": "2026-06-13T11:54:43.427958Z"
  }
}

Email — Sender Addresses

Register individual email addresses as authorised senders on a verified domain. Each sender must share the same domain you verified (e.g. [email protected], [email protected]). Multiple senders per domain are supported at no extra cost.

POST /v1/email/senders Register a sender address
Request Body application/json
ParameterTypeRequiredDescription
email_address string required Full sender address on a verified domain — e.g. [email protected].
display_name string optional Name shown in recipient's inbox — e.g. Company Name Support.
Request Body
{
  "email_address": "[email protected]",
  "display_name": "Company Name No-Reply"
}
201 CreatedSender Registered
{
  "success": true,
  "data": {
    "id": "61963e64-5df8-4a4e-aa2a-67106df20116",
    "email_address": "[email protected]",
    "display_name": "Company Name No-Reply",
    "is_active": true,
    "domain_name": "yourcompany.com",
    "domain_status": "verified",
    "created_at": "2026-06-14T06:57:59.180703Z"
  }
}
GET /v1/email/senders List registered senders
200 OKSender List
{
  "success": true,
  "data": [
    {
      "id": "b7ed458d-d279-4755-8f9e-b82348358bc3",
      "email_address": "[email protected]",
      "display_name": "Company Name",
      "is_active": true,
      "domain_name": "yourcompany.com",
      "domain_status": "verified",
      "created_at": "2026-06-13T16:19:22.256084Z"
    }
  ]
}

Email — Send Transactional Email

Send a transactional email from any registered sender address. Costs 1 credit per recipient — the to, each cc, and each bcc address all count separately. At least one of html or text is required.

POST /v1/email/send
Request Body Parameters application/json
ParameterTypeRequiredDescription
to string required Primary recipient email address.
from string required Registered sender address on a verified domain.
subject string required Email subject line (max 500 chars).
html string optional* HTML body. Required if text is omitted.
text string optional* Plain-text fallback. Required if html is omitted.
from_name string optional Display name shown in recipient's inbox (e.g. Company Name Billing).
reply_to string optional Address replies are routed to — can differ from from.
cc array optional CC addresses. Each entry costs 1 credit.
bcc array optional BCC addresses. Each entry costs 1 credit.
attachments array optional Up to 5 files, 5 MB each. Each object requires filename, content (base64), and content_type. See Attachment tab.
schedule_at string optional ISO 8601 datetime for future delivery. Naive datetimes (no Z/offset) are treated as EAT (UTC+3).
webhook_url string optional HTTPS URL to receive per-message delivery callbacks.
tags array optional String labels for filtering in message history (max 10).
metadata object optional Custom key-value data stored with the message (max 1 KB).
{
  "to": "[email protected]",
  "from": "[email protected]",
  "from_name": "Company Name Billing",
  "subject": "Your invoice is ready",
  "text": "Hi, your invoice #1234 is ready. Log in to view it.",
  "html": "<p>Hi, your invoice <strong>#1234</strong> is ready.</p>"
}
{
  "to": "[email protected]",
  "from": "[email protected]",
  "from_name": "Company Name Billing",
  "reply_to": "[email protected]",
  "cc": ["[email protected]"],
  "bcc": ["[email protected]"],
  "subject": "Invoice #1234 — copy to management",
  "text": "Please find your invoice details below.",
  "tags": ["invoice", "billing"],
  "metadata": {
    "invoice_id": "inv_1234",
    "customer_id": "cust_abc"
  }
}
// Attachment object fields:
//   filename    — original file name shown to recipient (e.g. "invoice.pdf")
//   content     — base64-encoded file bytes (standard encoding, no line breaks)
//   content_type — MIME type of the file
//
// Limits: max 5 attachments per email, 5 MB each.
// Allowed MIME types: application/pdf, application/msword,
//   application/vnd.openxmlformats-officedocument.wordprocessingml.document,
//   application/vnd.ms-excel,
//   application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,
//   application/vnd.ms-powerpoint,
//   application/vnd.openxmlformats-officedocument.presentationml.presentation,
//   application/zip, text/plain, text/csv,
//   image/jpeg, image/png, image/gif, image/webp
{
  "to": "[email protected]",
  "from": "[email protected]",
  "subject": "Invoice #1234 (PDF attached)",
  "text": "Please find your invoice attached.",
  "attachments": [
    {
      "filename": "invoice-1234.pdf",
      "content": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGg...",
      "content_type": "application/pdf"
    }
  ]
}
// Naive datetime (no Z or offset) → treated as EAT (Africa/Dar_es_Salaam, UTC+3)
// Explicit UTC:  "2026-06-14T07:37:00Z"
// Explicit EAT:  "2026-06-14T10:37:00+03:00"
// Naive (→ EAT): "2026-06-14T10:37:00"
{
  "to": "[email protected]",
  "from": "[email protected]",
  "from_name": "Company Name",
  "subject": "Your order ships tomorrow",
  "text": "Your order will be dispatched tomorrow morning.",
  "schedule_at": "2026-06-14T10:37:00"
}

Immediate send

200 OK
{
  "success": true,
  "data": {
    "id": "a9e6159f-086e-4a3c-b9f9-d144f4a0c8e2",
    "type": "email",
    "to": "[email protected]",
    "from": "[email protected]",
    "subject": "Your invoice is ready",
    "status": "sent",
    "recipient_count": 1,
    "credits_cost": 1,
    "credits_remaining": 3008,
    "queued_at": "2026-06-14T06:50:59.232037Z",
    "scheduled_at": null
  }
}

Scheduled send

200 OK
{
  "success": true,
  "data": {
    "id": "51545e29-c8d8-421f-af10-c76267b3d29d",
    "type": "email",
    "to": "[email protected]",
    "status": "scheduled",
    "recipient_count": 1,
    "credits_cost": 1,
    "credits_remaining": 3003,
    "queued_at": "2026-06-14T07:03:14.753227Z",
    "scheduled_at": "2026-06-14T07:37:00Z"
  }
}
Credits are deducted at send time, not scheduling time. A scheduled email with status: "scheduled" still reserves 1 credit per recipient in the response — the actual deduction occurs when the email is dispatched.

Email — Message History

Retrieve, filter, and resend email messages. The messages endpoint is shared across SMS and email — pass type=email to scope results. Each message includes a timeline object tracking queued, sent, and delivered timestamps.

GET /v1/messages?type=email List email messages
Query Parameters
ParameterTypeDefaultDescription
typestringPass email to filter to email messages only.
statusstringqueued, sent, delivered, failed, scheduled, cancelled
limitinteger20Page size (max 100).
offsetinteger0Pagination offset.
200 OKMessage List
{
  "success": true,
  "data": [
    {
      "id": "a9e6159f-086e-4a3c-b9f9-d144f4a0c8e2",
      "type": "email",
      "to": "[email protected]",
      "from": "[email protected]",
      "subject": "Your invoice is ready",
      "status": "sent",
      "credits_cost": 1,
      "created_at": "2026-06-14T06:50:59.245344Z"
    }
  ],
  "meta": {
    "total": 15,
    "limit": 20,
    "offset": 0,
    "has_more": false
  }
}
GET /v1/messages/{message_id} Full message detail + delivery timeline
200 OKMessage Detail
{
  "success": true,
  "data": {
    "id": "a9e6159f-086e-4a3c-b9f9-d144f4a0c8e2",
    "type": "email",
    "to": "[email protected]",
    "from": "[email protected]",
    "subject": "Your invoice is ready",
    "body": "Hi, your invoice #1234 is ready. Log in to view it.",
    "status": "sent",
    "segments": 1,
    "credits_cost": 1,
    "failure_reason": null,
    "timeline": {
      "queued_at": "2026-06-14T06:50:59.232037Z",
      "sent_at": "2026-06-14T06:51:00.799439Z",
      "delivered_at": null,
      "failed_at": null
    },
    "tags": [],
    "metadata": {},
    "webhook_url": null,
    "created_at": "2026-06-14T06:50:59.245344Z",
    "updated_at": "2026-06-14T06:50:59.218986Z"
  }
}
POST /v1/messages/{message_id}/resend Resend a failed email
Only messages with status: "failed" or "rejected" can be resent. Each resend charges 1 credit per original recipient_count.

No request body required. Returns the updated message object with status: "queued".

200 OKResend Accepted
{
  "success": true,
  "data": {
    "id": "a9e6159f-086e-4a3c-b9f9-d144f4a0c8e2",
    "status": "queued",
    "queued_at": "2026-06-14T08:15:03.441Z"
  }
}

Credits — Check Balance

Retrieve your organization's SMS and Email credit balances in a single call. Accepts either an API key or a user JWT token.

GET /v1/credits
# Using Authorization header
curl -X GET https://api.mojawave.com/v1/credits \
  -H "Authorization: Bearer mw_live_your_api_key_here"

# OR using X-API-Key header
curl -X GET https://api.mojawave.com/v1/credits \
  -H "X-API-Key: mw_live_your_api_key_here"
import requests

response = requests.get(
    "https://api.mojawave.com/v1/credits",
    headers={"Authorization": "Bearer mw_live_your_api_key_here"}
)
data = response.json()
print(data)
Response Schema (per service object)
FieldTypeDescription
service_typestringsms or email
balanceintegerCurrent available credits.
total_purchasedintegerLifetime credits purchased.
total_consumedintegerLifetime credits consumed.
low_balance_thresholdintegerAlert fires when balance drops below this value.
is_low_balancebooleantrue if balance is at or below the threshold.
200 OKCredit Balances
{
  "success": true,
  "data": {
    "sms": {
      "service_type": "sms",
      "balance": 5000,
      "total_purchased": 10000,
      "total_consumed": 5000,
      "low_balance_threshold": 500,
      "is_low_balance": false
    },
    "email": {
      "service_type": "email",
      "balance": 200,
      "total_purchased": 1000,
      "total_consumed": 800,
      "low_balance_threshold": 100,
      "is_low_balance": true
    }
  }
}
Both Authorization: Bearer mw_live_xxx and X-API-Key: mw_live_xxx headers are accepted. Rate limited to 120 requests/min.

Credits — Estimate Price

Quote the exact TZS price for a pay-as-you-go credit purchase before buying — nothing is created or charged. Pricing is whole-order tiered: the entire quantity is billed at the rate of the tier it falls into, so 20,000 SMS credits are all priced at the 15,001–30,000 tier rate.

GET /v1/billing/credits/estimate
Query Parameters
ParameterTypeRequiredDescription
sms_credits integer optional SMS credits to quote. At least one of sms_credits / email_credits is required.
email_credits integer optional Email credits to quote.
cURL
curl "https://api.mojawave.com/v1/billing/credits/estimate?sms_credits=20000" \
  -H "Authorization: Bearer mw_live_your_api_key_here"
200 OKPrice quote
{
  "success": true,
  "data": {
    "sms_credits": 20000,
    "sms_unit_price": "17",
    "sms_cost": "340000",
    "email_credits": 0,
    "email_unit_price": "0",
    "email_cost": "0",
    "total": "340000",
    "currency": "TZS",
    "min_payg_credits": 1000
  }
}
The minimum-purchase rule (min_payg_credits, currently 1,000 per service type) is not enforced here so you can show live prices as users type — the purchase endpoint enforces it when you actually buy.

Credits — Purchase via Mobile Money

Top up your SMS or Email pay-as-you-go credit balance directly from your own backend — no dashboard login required. Triggers a mobile money USSD push to the given phone number (Airtel Money, M-Pesa, Mixx by Yas, Halotel); credits are applied automatically once payment is confirmed, via webhook, with no manual review. This endpoint is PAYG only — bundle packages are a dashboard-only concept and aren't available here.

POST /v1/billing/credits/purchase/collect
Requires an API key with the credits:purchase scope enabled — this is opt-in only and not granted by default, since it spends real money. Enable it per-key from your dashboard's API Keys settings.
phone_number, customer_name, and customer_email are all optional — anything you omit defaults to your organization's own phone/name/email on file. Pass any of them explicitly to override on a per-request basis.
Request Body Parameters application/json
ParameterTypeRequiredDescription
sms_credits integer optional Pay-as-you-go SMS credits to buy. Minimum 1,000 if specified. At least one of sms_credits / email_credits is required.
email_credits integer optional Pay-as-you-go Email credits to buy. Minimum 1,000 if specified.
phone_number string optional Mobile money phone number: 255XXXXXXXXX or +255XXXXXXXXX. Defaults to your organization's phone number on file if omitted.
customer_name string optional Defaults to your organization's name on file if omitted (or the logged-in user's name for dashboard requests).
customer_email string optional Defaults to your organization's email on file if omitted (or the logged-in user's email for dashboard requests).
curl -X POST https://api.mojawave.com/v1/billing/credits/purchase/collect \
  -H "Authorization: Bearer mw_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "sms_credits": 1000
  }'
import requests

response = requests.post(
    "https://api.mojawave.com/v1/billing/credits/purchase/collect",
    headers={"Authorization": "Bearer mw_live_your_api_key_here"},
    json={"sms_credits": 1000}
)
data = response.json()
print(data)
201 CreatedPurchase initiated
{
  "success": true,
  "data": {
    "transaction_id": "cdd8504b-ea49-4050-9a97-3e75166779cd",
    "checkout_reference": "MW17843894124082592",
    "status": "pending",
    "amount": "18000.00",
    "currency": "TZS",
    "customer_message": "Check your phone and enter your mobile money PIN to complete payment.",
    "expires_at": "2026-07-18T15:53:32.408433Z"
  }
}
Polling fallback: GET /v1/billing/credits/purchase/collect/{transaction_id}/status returns the transaction's current status if you'd rather poll than wait on a webhook.
Subscribe to the credits.purchase.completed webhook event (see Webhooks) to be notified the moment payment is confirmed and credits are applied — polling the status endpoint is the fallback. credits.purchase.failed fires if the payment fails or expires, and credits.low notifies you when your balance runs low again.

Webhooks — Overview & Events

MojaWave sends HTTP POST requests to your callback URL when events occur. All webhook bodies are JSON with a consistent envelope structure.

Webhook envelope
{
  "id": "evt_3r9qhz7b1k",
  "type": "message.delivered",
  "created_at": "2026-04-05T12:05:01Z",
  "livemode": true,
  "data": {
    "id": "89b82624-f1a2-4f5e-85b5-102e79a06779",
    "status": "delivered",
    "to": "+255753276939"
    // ... full resource object
  }
}
message.sent
Message was accepted by the carrier.
message.delivered
Message was delivered to the recipient.
message.failed
Message delivery failed permanently.
credits.low
Credit balance dropped below threshold.
credits.purchase.completed
Credit purchase was paid and credits applied.
credits.purchase.failed
Credit purchase failed, expired, or was cancelled.

Webhooks — Signature Verification

Every webhook includes an X-MojaWave-Signature header containing an HMAC-SHA256 signature. Always verify this before processing events to protect against spoofed requests.

Verify signatures using the raw request body before any JSON parsing. Parsing first may alter whitespace and invalidate the signature check.
import hmac, hashlib

def verify_signature(payload: bytes, sig: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

# In your Flask/Django view:
sig = request.headers.get("X-MojaWave-Signature")
if not verify_signature(request.get_data(), sig, WEBHOOK_SECRET):
    return "Forbidden", 403

event = request.get_json()
# Safe to process event
import { createHmac, timingSafeEqual } from 'crypto';

function verifySignature(payload, sig, secret) {
  const expected = createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(sig)
  );
}

// Express middleware (raw body required)
app.post('/webhooks', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-mojawave-signature'];
  if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(403).send('Forbidden');
  }
  const event = JSON.parse(req.body);
  // Safe to process
});
<?php
$payload = file_get_contents('php://input');
$sig     = $_SERVER['HTTP_X_MOJAWAVE_SIGNATURE'] ?? '';
$secret  = getenv('WEBHOOK_SECRET');

$expected = hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $sig)) {
    http_response_code(403);
    exit('Forbidden');
}

$event = json_decode($payload, true);
// Safe to process $event

AI Integration · MCP

Model Context Protocol (MCP)

The mojawave-mcp package exposes every MojaWave API as a native tool for MCP-compatible AI assistants — Claude (Desktop & Code), ChatGPT (OpenAI Agents SDK), Gemini (Google ADK), Cursor, Windsurf, and any other tool that speaks the Model Context Protocol. Inputs are validated before any request is made, and the client retries 429/5xx responses with backoff automatically.

Installation

bash
pip install mojawave-mcp

Available tools

ToolAPI endpointWhat it does
SMS
list_sms_sender_idsGET /sms/sender-ids/approvedList approved sender IDs — call before sending to pick the right sender_id.
send_smsPOST /sms/sendSend a single SMS, optionally scheduled (schedule_at).
send_bulk_smsPOST /sms/bulkStart an async bulk SMS job for up to 10,000 recipients — returns a job_id.
get_bulk_sms_jobGET /sms/bulk/{id}Poll the status and progress of a bulk SMS job.
Email
list_email_domainsGET /email/domainsList sending domains and verification status — confirm a domain is verified before sending.
list_email_sendersGET /email/sendersList registered sender addresses available as from_email.
send_emailPOST /email/sendSend a transactional email — supports HTML, CC/BCC, reply-to, scheduled delivery, and tags.
Account
get_messageGET /messages/{id}Get full details and delivery timeline for a single message.
get_credit_balanceGET /creditsCheck current SMS and email credit balances.
verify_webhook_signatureVerify a webhook's X-MojaWave-Signature (HMAC-SHA256) locally — no API call needed.

Claude Desktop

Add this block to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows), then restart Claude Desktop.

json
{
  "mcpServers": {
    "mojawave": {
      "command": "mojawave-mcp",
      "env": {
        "MOJAWAVE_API_KEY": "mw_xxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}

Claude Code (CLI)

bash
claude mcp add mojawave -- env MOJAWAVE_API_KEY=mw_xxx mojawave-mcp

Cursor / Windsurf / any stdio MCP client

Most clients use the same JSON config format as Claude Desktop above — point the client at the mojawave-mcp command with your API key as an environment variable and refer to your client's MCP documentation.

OpenAI Agents SDK (ChatGPT / GPT-4o)

Start the server in SSE mode so OpenAI can reach it over HTTP:

bash
MOJAWAVE_API_KEY=mw_xxx mojawave-mcp --transport sse --port 8080

Then connect from Python:

python
from agents import Agent, Runner
from agents.mcp import MCPServerSse

async def main():
    server = MCPServerSse(url="http://localhost:8080/sse")
    async with server:
        agent = Agent(
            name="MojaWave Agent",
            model="gpt-4o",
            mcp_servers=[server],
        )
        result = await Runner.run(
            agent, "Send an SMS to +255712345678 saying Hello from AI"
        )
        print(result.final_output)

Google Gemini (Google ADK)

Start the server in SSE mode (same command as above), then connect from Python:

python
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, SseServerParams

mojawave_tools = MCPToolset(
    connection_params=SseServerParams(url="http://localhost:8080/sse")
)

agent = LlmAgent(
    model="gemini-2.0-flash",
    name="mojawave_agent",
    instruction="You can send SMS and transactional email and check credits via MojaWave.",
    tools=[mojawave_tools],
)

Hosted deployment (Docker)

Run the SSE server behind a reverse proxy for production use:

dockerfile
FROM python:3.12-slim
RUN pip install mojawave-mcp
ENV MOJAWAVE_API_KEY=""
EXPOSE 8080
CMD ["mojawave-mcp", "--transport", "sse", "--port", "8080"]
bash
docker build -t mojawave-mcp .
docker run -e MOJAWAVE_API_KEY=mw_xxx -p 8080:8080 mojawave-mcp
Use a test key (sk_test_mw_…) during development — it returns synthetic responses without sending real messages or charging credits.

Sandbox & Testing

The sandbox environment mirrors the live API exactly, but no real transactions or SMS are sent. Use your sk_test_mw_ key and set environment: "sandbox" in your SDK config.

Sandbox webhooks fire within 2–5 seconds of the API call. You can use webhook.site or ngrok to receive them locally during development.

Ready to build?

Get sandbox API keys and integrate in minutes.