SMOO Pay API
Accept M-Pesa and e-Mola payments in Mozambique programmatically. Create charges, track payment status, and receive webhooks when payments complete.
Base URL: https://smoopay.app/api/v1
Response format: All responses are JSON. Errors use a consistent shape: { "error": "message" }
Rate limits: 60 requests per minute per API key. Exceeding this returns 429.
Authentication
All API requests require a Bearer token in the Authorization header. Generate your API key in Settings → API → Generate key.
Production keys start with sk_live_ and process real payments through M-Pesa / e-Mola.
Sandbox keys start with sk_test_. Charges created with test keys auto-complete without hitting payment providers, so you can develop and test safely.
Header format
Authorization: Bearer sk_live_xxxxxExample request
curl https://smoopay.app/api/v1/charges \
-H "Authorization: Bearer sk_live_your_api_key_here"Create a Charge
/api/v1/chargesCreates a new payment charge. Two modes are available:
- Direct payment — Provide
customer_phoneandprovider. The customer receives an STK push to confirm the payment immediately. - Checkout URL — Omit phone and provider. A
checkout_urlis returned for the customer to choose their provider and complete payment.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Required | Amount in MZN. Must be a positive number (e.g. 150.00). |
title | string | Required | Payment title or product name. |
description | string | Optional | Optional description for the payment. |
customer_phone | string | Optional | Customer phone number (e.g. 841234567). Triggers direct payment when paired with provider. |
provider | "MPESA" | "EMOLA" | Optional | Payment provider. Required when customer_phone is set. |
callback_url | string | Optional | HTTPS URL for webhook notifications on status change. |
redirect_url | string | Optional | URL to redirect the customer after completing checkout. |
metadata | object | Optional | Arbitrary key-value pairs stored with the charge for your reference. |
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
Authorization | string | Required | Bearer token with your API key. |
Content-Type | string | Required | Must be application/json. |
Idempotency-Key | string | Optional | Unique key to prevent duplicate charges. Same key within 24 hours returns the original charge. |
Example: Direct Payment (curl)
curl -X POST https://smoopay.app/api/v1/charges \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"amount": 150.00,
"title": "Premium Plan",
"customer_phone": "841234567",
"provider": "MPESA",
"callback_url": "https://your-server.com/webhooks/smoo",
"metadata": { "user_id": "abc123" }
}'Response (Direct Payment)
{
"id": "cLr8x3kWpN2mYvBq",
"status": "COMPLETED",
"amount": 150.00,
"currency": "MZN",
"provider": "MPESA",
"transaction_id": "tx_7f3a9c1e"
}Example: Checkout URL (curl)
curl -X POST https://smoopay.app/api/v1/charges \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"amount": 250.00,
"title": "Course Access",
"callback_url": "https://your-server.com/webhooks/smoo"
}'Response (Checkout URL)
{
"id": "dPq4y7nRsT5kJwAm",
"status": "PENDING",
"amount": 250.00,
"currency": "MZN",
"checkout_url": "https://smoopay.app/pay/charge/dPq4y7nRsT5kJwAm"
}Example: JavaScript (fetch)
const response = await fetch(
"https://smoopay.app/api/v1/charges",
{
method: "POST",
headers: {
Authorization: "Bearer sk_live_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 500.0,
title: "Annual subscription",
customer_phone: "861234567",
provider: "EMOLA",
callback_url: "https://your-server.com/webhooks/smoo",
metadata: { plan: "annual" },
}),
}
);
const charge = await response.json();
console.log(charge.id, charge.status);List Charges
/api/v1/chargesReturns a paginated list of your charges, sorted by creation date (newest first).
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by status: PENDING, COMPLETED, FAILED, CANCELLED, EXPIRED. |
limit | number | Optional | Max results per page (1-100, default 20). |
cursor | string | Optional | Charge ID for cursor-based pagination. Pass the last charge ID from the previous page. |
Example
curl "https://smoopay.app/api/v1/charges?status=COMPLETED&limit=10" \
-H "Authorization: Bearer sk_live_your_api_key"Response
{
"data": [
{
"id": "cLr8x3kWpN2mYvBq",
"status": "COMPLETED",
"amount": 150.00,
"currency": "MZN",
"title": "Premium Plan",
"description": null,
"provider": "MPESA",
"customer_phone": "841234567",
"metadata": { "user_id": "abc123" },
"created_at": "2025-06-15T10:30:00.000Z",
"updated_at": "2025-06-15T10:30:05.000Z"
}
],
"cursor": "cLr8x3kWpN2mYvBq",
"has_more": false
}Get a Charge
/api/v1/charges/:idRetrieve the full details of a specific charge by its ID.
Example
curl https://smoopay.app/api/v1/charges/cLr8x3kWpN2mYvBq \
-H "Authorization: Bearer sk_live_your_api_key"Response
{
"id": "cLr8x3kWpN2mYvBq",
"status": "COMPLETED",
"amount": 150.00,
"currency": "MZN",
"title": "Premium Plan",
"description": null,
"provider": "MPESA",
"customer_phone": "841234567",
"transaction_id": "tx_7f3a9c1e",
"failure_reason": null,
"callback_url": "https://your-server.com/webhooks/smoo",
"redirect_url": null,
"checkout_url": null,
"metadata": { "user_id": "abc123" },
"created_at": "2025-06-15T10:30:00.000Z",
"updated_at": "2025-06-15T10:30:05.000Z"
}Update a Charge
/api/v1/charges/:idUpdate a charge that is still in PENDING status. You can modify the metadata and expiration time.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
metadata | object | Optional | New metadata object. Replaces the existing metadata entirely. |
expires_at | string | Optional | ISO 8601 timestamp for when the charge should expire. |
Example
curl -X PATCH https://smoopay.app/api/v1/charges/dPq4y7nRsT5kJwAm \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"metadata": { "order_id": "ORD-2025-100", "note": "Updated by admin" },
"expires_at": "2025-07-01T00:00:00.000Z"
}'Response
{
"id": "dPq4y7nRsT5kJwAm",
"status": "PENDING",
"amount": 250.00,
"currency": "MZN",
"metadata": { "order_id": "ORD-2025-100", "note": "Updated by admin" },
"expires_at": "2025-07-01T00:00:00.000Z",
"updated_at": "2025-06-16T14:22:00.000Z"
}PENDING status can be updated. Attempting to update a completed, failed, or cancelled charge returns a 400 error.Cancel a Charge
/api/v1/charges/:id/cancelCancels a charge that is still in PENDING status. This prevents the customer from completing the payment.
Example
curl -X POST https://smoopay.app/api/v1/charges/dPq4y7nRsT5kJwAm/cancel \
-H "Authorization: Bearer sk_live_your_api_key"Response
{
"id": "dPq4y7nRsT5kJwAm",
"status": "CANCELLED",
"amount": 250.00,
"currency": "MZN",
"cancelled_at": "2025-06-16T15:00:00.000Z"
}PENDING status can be cancelled. A charge.cancelled webhook event is fired if a callback_url was set.Webhooks
When a charge changes status, SMOO sends a POST request to the callback_url you specified when creating the charge. The request body is JSON and includes an HMAC-SHA256 signature so you can verify it came from SMOO.
Signature verification
Each webhook includes an X-Smoo-Signature header containing an HMAC-SHA256 hex digest of the raw JSON body, signed with your webhook secret. Set your webhook secret in Settings → API.
Events
charge.completedPayment was successfulcharge.failedPayment failed (provider error or customer cancelled on device)charge.cancelledCharge was cancelled via the API or dashboardcharge.expiredCharge expired before the customer paidtest.pingTest event fired from the webhook test endpointWebhook payload
{
"event": "charge.completed",
"data": {
"id": "cLr8x3kWpN2mYvBq",
"status": "COMPLETED",
"amount": 150.00,
"currency": "MZN",
"provider": "MPESA",
"transaction_id": "tx_7f3a9c1e",
"metadata": { "user_id": "abc123" }
},
"timestamp": "2025-06-15T10:30:05.000Z"
}Verifying signatures (Node.js)
const crypto = require("crypto");
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post("/webhooks/smoo", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-smoo-signature"];
if (!verifyWebhook(req.body, signature, process.env.SMOO_WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const { event, data } = JSON.parse(req.body);
switch (event) {
case "charge.completed":
// Fulfil the order
console.log("Payment completed:", data.id);
break;
case "charge.failed":
// Notify your customer
console.log("Payment failed:", data.id, data.failure_reason);
break;
}
res.status(200).send("OK");
});Retry policy
Test endpoint
Send a test webhook to verify your server is receiving events correctly.
/api/v1/webhooks/testFires a test.ping event to your configured webhook URL. Returns immediately with the delivery status.
curl -X POST https://smoopay.app/api/v1/webhooks/test \
-H "Authorization: Bearer sk_live_your_api_key"Tracking & UTMs
SMOO Pay captures attribution parameters from the buyer's URL when they land on the checkout, persists them on the transaction, and forwards them to Facebook Conversions API and UTMify when the payment completes. Captured automatically:
utm_source,utm_medium,utm_campaign,utm_content,utm_termfbclid— Facebook click ID, automatically appended to every Facebook ad click. Forwarded asuser_data.fbcon the CAPI Purchase event for click-to-conversion matching.gclid— Google click ID, persisted on the transaction.
Cross-domain funnel: forward UTMs to checkout
If your funnel sends the buyer through your own domain first (quiz → VSL → SMOO checkout), UTMs and click IDs from the original ad will be lost on the redirect to smoopay.appunless your last page explicitly forwards them. Drop this snippet on your VSL / order page:
<!-- On the page that links to your SMOO checkout -->
<script>
(function () {
var src = new URLSearchParams(window.location.search);
var keep = ["utm_source","utm_medium","utm_campaign","utm_content","utm_term","fbclid","gclid"];
document.querySelectorAll('a[href*="smoopay.app"]').forEach(function (a) {
try {
var u = new URL(a.href);
keep.forEach(function (k) {
var v = src.get(k);
if (v && !u.searchParams.get(k)) u.searchParams.set(k, v);
});
a.href = u.toString();
} catch (_) {}
});
})();
</script>Or, if you redirect via JavaScript (e.g. on a button click):
function goToCheckout(slug) {
var dest = new URL("https://smoopay.app/pay/" + slug);
var src = new URLSearchParams(window.location.search);
["utm_source","utm_medium","utm_campaign","utm_content","utm_term","fbclid","gclid"]
.forEach(function (k) {
var v = src.get(k);
if (v) dest.searchParams.set(k, v);
});
window.location = dest.toString();
}Tagging your ad URLs
Build the destination URL with UTMs in your ad platform. Examples:
Facebook / Instagram:
https://smoopay.app/pay/SEVEN?utm_source=facebook&utm_medium=paid&utm_campaign=summer-2026
Google Ads:
https://smoopay.app/pay/SEVEN?utm_source=google&utm_medium=cpc&utm_campaign=brand
Email / WhatsApp:
https://smoopay.app/pay/SEVEN?utm_source=email&utm_medium=newsletter&utm_campaign=launchfbclid is appended automatically by Facebook on every ad click — you do not need to add it manually. Same for gclid when Google's auto-tagging is enabled.
Providers
SMOO supports two mobile money providers in Mozambique. All amounts are in MZN (Metical mocambicano).
M-Pesa
Provider value: MPESA
Phone prefixes: 84, 85
Example: 841234567, 851234567
e-Mola
Provider value: EMOLA
Phone prefixes: 86, 87
Example: 861234567, 871234567
Payment flow
- Your server creates a charge via
POST /api/v1/chargeswith the customer's phone and provider. - SMOO sends an STK push to the customer's phone. They see a prompt to confirm the payment.
- The customer enters their PIN on their device to authorize.
- The provider processes the transaction. SMOO updates the charge status.
- A webhook fires to your
callback_urlwith the final status (charge.completedorcharge.failed).
Error Codes
All errors return a JSON body with an error field describing what went wrong.
| Status | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid amount, invalid provider, callback_url not HTTPS. |
| 401 | Unauthorized | Missing or invalid API key. Ensure the Authorization header is correct. |
| 404 | Not Found | Charge does not exist or is not owned by your account. |
| 409 | Conflict | Idempotency conflict. A charge with the same Idempotency-Key already exists with different parameters. |
| 429 | Too Many Requests | Rate limit exceeded (60 req/min). Wait and retry with exponential backoff. |
| 500 | Internal Server Error | Unexpected error on our side. Retry the request or contact support. |
Error response format
{
"error": "amount is required and must be a positive number"
}Full JavaScript Example
A complete Node.js example that creates a charge, polls for status, and handles webhooks.
const SMOO_API_KEY = "sk_live_your_api_key";
const BASE_URL = "https://smoopay.app/api/v1";
// Create a direct M-Pesa charge
async function createDirectCharge() {
const res = await fetch(BASE_URL + "/charges", {
method: "POST",
headers: {
Authorization: "Bearer " + SMOO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 350.0,
title: "Monthly subscription",
customer_phone: "851234567",
provider: "MPESA",
callback_url: "https://your-server.com/webhooks/smoo",
metadata: { customer_id: "cust_42" },
}),
});
return res.json();
}
// Create a checkout URL (customer picks provider)
async function createCheckout() {
const res = await fetch(BASE_URL + "/charges", {
method: "POST",
headers: {
Authorization: "Bearer " + SMOO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 1200.0,
title: "E-commerce order #1042",
redirect_url: "https://your-store.com/order/1042/thank-you",
callback_url: "https://your-server.com/webhooks/smoo",
}),
});
const charge = await res.json();
// Redirect your customer to charge.checkout_url
return charge;
}
// Get charge details
async function getCharge(chargeId) {
const res = await fetch(BASE_URL + "/charges/" + chargeId, {
headers: { Authorization: "Bearer " + SMOO_API_KEY },
});
return res.json();
}
// List completed charges
async function listCompletedCharges() {
const res = await fetch(BASE_URL + "/charges?status=COMPLETED&limit=20", {
headers: { Authorization: "Bearer " + SMOO_API_KEY },
});
return res.json();
}
// Cancel a pending charge
async function cancelCharge(chargeId) {
const res = await fetch(BASE_URL + "/charges/" + chargeId + "/cancel", {
method: "POST",
headers: { Authorization: "Bearer " + SMOO_API_KEY },
});
return res.json();
}