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

text
Authorization: Bearer sk_live_xxxxx

Example request

bash
curl https://smoopay.app/api/v1/charges \
  -H "Authorization: Bearer sk_live_your_api_key_here"
Important: Your API key is shown only once when generated. Store it securely. If lost, generate a new one (the old key is immediately invalidated).

Create a Charge

POST/api/v1/charges

Creates a new payment charge. Two modes are available:

  • Direct payment — Provide customer_phone and provider. The customer receives an STK push to confirm the payment immediately.
  • Checkout URL — Omit phone and provider. A checkout_url is returned for the customer to choose their provider and complete payment.

Request Body

ParameterTypeRequiredDescription
amountnumberRequiredAmount in MZN. Must be a positive number (e.g. 150.00).
titlestringRequiredPayment title or product name.
descriptionstringOptionalOptional description for the payment.
customer_phonestringOptionalCustomer phone number (e.g. 841234567). Triggers direct payment when paired with provider.
provider"MPESA" | "EMOLA"OptionalPayment provider. Required when customer_phone is set.
callback_urlstringOptionalHTTPS URL for webhook notifications on status change.
redirect_urlstringOptionalURL to redirect the customer after completing checkout.
metadataobjectOptionalArbitrary key-value pairs stored with the charge for your reference.

Request Headers

ParameterTypeRequiredDescription
AuthorizationstringRequiredBearer token with your API key.
Content-TypestringRequiredMust be application/json.
Idempotency-KeystringOptionalUnique key to prevent duplicate charges. Same key within 24 hours returns the original charge.

Example: Direct Payment (curl)

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

json
{
  "id": "cLr8x3kWpN2mYvBq",
  "status": "COMPLETED",
  "amount": 150.00,
  "currency": "MZN",
  "provider": "MPESA",
  "transaction_id": "tx_7f3a9c1e"
}

Example: Checkout URL (curl)

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

json
{
  "id": "dPq4y7nRsT5kJwAm",
  "status": "PENDING",
  "amount": 250.00,
  "currency": "MZN",
  "checkout_url": "https://smoopay.app/pay/charge/dPq4y7nRsT5kJwAm"
}

Example: JavaScript (fetch)

javascript
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

GET/api/v1/charges

Returns a paginated list of your charges, sorted by creation date (newest first).

Query Parameters

ParameterTypeRequiredDescription
statusstringOptionalFilter by status: PENDING, COMPLETED, FAILED, CANCELLED, EXPIRED.
limitnumberOptionalMax results per page (1-100, default 20).
cursorstringOptionalCharge ID for cursor-based pagination. Pass the last charge ID from the previous page.

Example

bash
curl "https://smoopay.app/api/v1/charges?status=COMPLETED&limit=10" \
  -H "Authorization: Bearer sk_live_your_api_key"

Response

json
{
  "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

GET/api/v1/charges/:id

Retrieve the full details of a specific charge by its ID.

Example

bash
curl https://smoopay.app/api/v1/charges/cLr8x3kWpN2mYvBq \
  -H "Authorization: Bearer sk_live_your_api_key"

Response

json
{
  "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

PATCH/api/v1/charges/:id

Update a charge that is still in PENDING status. You can modify the metadata and expiration time.

Request Body

ParameterTypeRequiredDescription
metadataobjectOptionalNew metadata object. Replaces the existing metadata entirely.
expires_atstringOptionalISO 8601 timestamp for when the charge should expire.

Example

bash
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

json
{
  "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"
}
Note: Only charges with PENDING status can be updated. Attempting to update a completed, failed, or cancelled charge returns a 400 error.

Cancel a Charge

POST/api/v1/charges/:id/cancel

Cancels a charge that is still in PENDING status. This prevents the customer from completing the payment.

Example

bash
curl -X POST https://smoopay.app/api/v1/charges/dPq4y7nRsT5kJwAm/cancel \
  -H "Authorization: Bearer sk_live_your_api_key"

Response

json
{
  "id": "dPq4y7nRsT5kJwAm",
  "status": "CANCELLED",
  "amount": 250.00,
  "currency": "MZN",
  "cancelled_at": "2025-06-16T15:00:00.000Z"
}
Note: Only charges with 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 successful
charge.failedPayment failed (provider error or customer cancelled on device)
charge.cancelledCharge was cancelled via the API or dashboard
charge.expiredCharge expired before the customer paid
test.pingTest event fired from the webhook test endpoint

Webhook payload

json
{
  "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)

javascript
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

Retries: Webhooks are retried up to 3 times with exponential backoff (1 s, 5 s, 15 s) if your server returns a non-2xx response or is unreachable.

Test endpoint

Send a test webhook to verify your server is receiving events correctly.

POST/api/v1/webhooks/test

Fires a test.ping event to your configured webhook URL. Returns immediately with the delivery status.

bash
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_term
  • fbclid — Facebook click ID, automatically appended to every Facebook ad click. Forwarded as user_data.fbc on 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:

html
<!-- 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):

javascript
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:

text
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=launch

fbclid 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

  1. Your server creates a charge via POST /api/v1/charges with the customer's phone and provider.
  2. SMOO sends an STK push to the customer's phone. They see a prompt to confirm the payment.
  3. The customer enters their PIN on their device to authorize.
  4. The provider processes the transaction. SMOO updates the charge status.
  5. A webhook fires to your callback_url with the final status (charge.completed or charge.failed).
Tip: When using the checkout URL flow (no phone / provider), the customer picks their provider on the hosted payment page. This is the simplest integration path.

Error Codes

All errors return a JSON body with an error field describing what went wrong.

StatusMeaningCommon causes
400Bad RequestMissing required fields, invalid amount, invalid provider, callback_url not HTTPS.
401UnauthorizedMissing or invalid API key. Ensure the Authorization header is correct.
404Not FoundCharge does not exist or is not owned by your account.
409ConflictIdempotency conflict. A charge with the same Idempotency-Key already exists with different parameters.
429Too Many RequestsRate limit exceeded (60 req/min). Wait and retry with exponential backoff.
500Internal Server ErrorUnexpected error on our side. Retry the request or contact support.

Error response format

json
{
  "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.

javascript
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();
}