Quick start
- Create an account, then open API keys in the dashboard and generate a key. Copy it immediately: it is shown only once.
- Fund your wallet. API orders are charged to it, exactly like orders on the website.
- Send requests to the base URL below with your key in the
Authorizationheader.
All requests and responses use JSON. Keep your key on your server: never put it in browser JavaScript or a mobile app.
Authentication
Send your secret key as a Bearer token. The X-API-Key: <key> header is also accepted.
Authorization: Bearer mhs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxKeys can be revoked at any time from the API keys page. A revoked or mistyped key returns 401 invalid_api_key.
Responses & errors
Every response has a success flag. Successful responses carry data; failures carry an error object with a stable code you can branch on.
{
"success": true,
"data": { "...": "..." }
}{
"success": false,
"error": {
"code": "insufficient_balance",
"message": "Your wallet balance is too low for this order.",
"balance": "3.00",
"required": "25.00"
}
}Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | validation_error | A field is missing or invalid. field names it. |
| 400 | invalid_json | The body is not a JSON object. |
| 400 | invalid_network | Unknown network code. valid lists the accepted codes. |
| 401 | invalid_api_key | Missing, mistyped or revoked key. |
| 402 | insufficient_balance | Wallet too low. Top up and retry with the same reference. |
| 403 | account_inactive | The account behind the key is disabled. |
| 404 | package_not_found / order_not_found | No such active package, or no such order on your account. |
| 405 | method_not_allowed | Wrong HTTP method for the endpoint. |
| 429 | rate_limited | Too many requests this minute. |
| 503 | shop_closed | Outside business hours. message gives the hours. |
| 503 | network_unavailable | That network is down or paused. |
| 503 | api_disabled | The API has been switched off by the administrator. |
Rate limits
Each key may make 120 requests per minute. Every request is logged and visible on your API keys page. Money values are strings with two decimals, in GHS.
Account
Returns your username, account type (customer, agent, dealer) and wallet balance.
cURL
curl https://mallhouseservices.com/api/v1/account/ \
-H "Authorization: Bearer $MHS_API_KEY"
Python
import os
import requests
API_KEY = os.environ["MHS_API_KEY"]
BASE_URL = "https://mallhouseservices.com/api/v1"
response = requests.get(
f"{BASE_URL}/account/",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
response.raise_for_status()
account = response.json()["data"]
print(account["balance"], account["currency"])
Node.js
// Node.js 18+ (built-in fetch)
const API_KEY = process.env.MHS_API_KEY;
const BASE_URL = "https://mallhouseservices.com/api/v1";
const res = await fetch(`${BASE_URL}/account/`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const body = await res.json();
if (!body.success) throw new Error(body.error.message);
console.log(body.data.balance, body.data.currency);
PHP
<?php
$apiKey = getenv('MHS_API_KEY');
$ch = curl_init('https://mallhouseservices.com/api/v1/account/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
CURLOPT_TIMEOUT => 15,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $body['data']['balance'] . ' ' . $body['data']['currency'];
{
"success": true,
"data": {
"username": "kofi_shop",
"account_type": "agent",
"balance": "248.50",
"currency": "GHS"
}
}Networks
Lists every network with its live status and whether orders are being accepted. Network codes:
mtn: MTNtelecel: Telecelairteltigo_ishare: AirtelTigo iShareairteltigo_bigtime: AirtelTigo Big Time
Packages
Active bundles, priced for your account (agent, dealer and custom prices apply). network is optional.
cURL
curl "https://mallhouseservices.com/api/v1/packages/?network=mtn" \
-H "Authorization: Bearer $MHS_API_KEY"
Python
response = requests.get(
f"{BASE_URL}/packages/",
params={"network": "mtn"},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
for package in response.json()["data"]:
print(package["id"], package["size_gb"], "GB", package["price"])
Node.js
const res = await fetch(`${BASE_URL}/packages/?network=mtn`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { data: packages } = await res.json();
packages.forEach((p) => console.log(p.id, `${p.size_gb}GB`, p.price));
PHP
<?php
$ch = curl_init('https://mallhouseservices.com/api/v1/packages/?network=mtn');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
]);
$packages = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);
foreach ($packages as $p) {
echo "{$p['id']}: {$p['size_gb']}GB - {$p['price']}\n";
}
{
"success": true,
"data": [
{ "id": 12, "network": "mtn", "network_name": "MTN",
"size_gb": 5.0, "price": "26.13", "currency": "GHS" }
]
}Place an order
Debits your wallet and queues the bundle. Returns 201 with the new order. Identify the bundle with package_id, or with network plus size_gb.
Body parameters
| Field | Type | Required? | Description |
|---|---|---|---|
phone_number | string | Required | Recipient number, e.g. 0241234567. +233 and 9-digit forms are accepted. |
package_id | integer | Required unless network + size_gb are sent | From List packages. |
network | string | With size_gb | A network code such as mtn. |
size_gb | number | With network | Bundle size in GB, e.g. 5. |
reference | string | Optional (recommended) | Your own unique ID, up to 64 characters. Sending the same reference again returns the original order with "duplicate": true and never charges twice, so retries are safe. |
cURL
curl -X POST https://mallhouseservices.com/api/v1/orders/ \
-H "Authorization: Bearer $MHS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"network": "mtn",
"size_gb": 5,
"phone_number": "0241234567",
"reference": "INV-1001"
}'
Python
payload = {
"network": "mtn", # or send "package_id": 12 instead
"size_gb": 5,
"phone_number": "0241234567",
"reference": "INV-1001", # your own unique ID - safe to retry
}
response = requests.post(
f"{BASE_URL}/orders/",
json=payload,
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
body = response.json()
if body["success"]:
order = body["data"]
print("Order", order["id"], "is", order["status"])
else:
print("Failed:", body["error"]["code"], body["error"]["message"])
Node.js
const res = await fetch(`${BASE_URL}/orders/`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
network: "mtn", // or package_id: 12
size_gb: 5,
phone_number: "0241234567",
reference: "INV-1001", // your own unique ID - safe to retry
}),
});
const body = await res.json();
if (body.success) {
console.log(`Order ${body.data.id} is ${body.data.status}`);
} else {
console.error(body.error.code, body.error.message);
}
PHP
<?php
$payload = json_encode([
'network' => 'mtn', // or 'package_id' => 12
'size_gb' => 5,
'phone_number' => '0241234567',
'reference' => 'INV-1001', // your own unique ID - safe to retry
]);
$ch = curl_init('https://mallhouseservices.com/api/v1/orders/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiKey",
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 30,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($body['success']) {
echo "Order {$body['data']['id']} is {$body['data']['status']}";
} else {
echo "Failed: {$body['error']['code']} - {$body['error']['message']}";
}
{
"success": true,
"data": {
"id": 42,
"reference": "INV-1001",
"status": "pending",
"network": "mtn",
"network_name": "MTN",
"package_id": 12,
"size_gb": 5.0,
"phone_number": "0241234567",
"price": "26.13",
"currency": "GHS",
"balance_after": "222.37",
"created_at": "2026-10-30T09:15:02.120Z",
"updated_at": "2026-10-30T09:15:02.120Z"
}
}List orders
Your orders, newest first, including ones placed on the website. All query parameters are optional: status (pending, processing, complete, cancelled), reference, page, and per_page (up to 100). The response includes a pagination object with total and pages.
Get an order
Fetch one order by its id. To look an order up by your own reference, use /api/v1/orders/?reference=INV-1001.
cURL
# By id
curl https://mallhouseservices.com/api/v1/orders/42/ \
-H "Authorization: Bearer $MHS_API_KEY"
# By your own reference
curl "https://mallhouseservices.com/api/v1/orders/?reference=INV-1001" \
-H "Authorization: Bearer $MHS_API_KEY"
Python
response = requests.get(
f"{BASE_URL}/orders/42/",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=15,
)
print(response.json()["data"]["status"]) # pending | processing | complete | cancelled
Node.js
const res = await fetch(`${BASE_URL}/orders/?reference=INV-1001`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { data } = await res.json();
console.log(data[0]?.status);
PHP
<?php
$ch = curl_init('https://mallhouseservices.com/api/v1/orders/42/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
]);
$order = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);
echo $order['status'];
Webhooks
Add an https:// webhook URL to a key on the API keys page. Whenever an order placed with that key changes status, we POST the event to you:
- Events:
order.processing,order.complete,order.cancelled(cancelled orders are refunded to your wallet). - Header
X-MHS-Signature: sha256=<hex>is an HMAC-SHA256 of the raw request body using the key's webhook secret. Always verify it. - Reply with any
2xxwithin 8 seconds. Deliveries are not retried, so poll Get an order as a fallback.
{
"event": "order.complete",
"sent_at": "2026-10-30T09:21:44.501Z",
"data": { "id": 42, "reference": "INV-1001", "status": "complete", "...": "..." }
}cURL
# Simulate a delivery to your own endpoint while developing:
BODY='{"event":"order.complete","data":{"id":42,"status":"complete"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$MHS_WEBHOOK_SECRET" | sed 's/^.* //')
curl -X POST https://example.com/webhooks/mhs \
-H "Content-Type: application/json" \
-H "X-MHS-Signature: sha256=$SIG" \
-d "$BODY"
Python
# Flask example
import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["MHS_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/mhs")
def mhs_webhook():
expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-MHS-Signature", "")):
abort(401)
event = request.get_json()
order = event["data"]
print(event["event"], order["id"], order["reference"])
return "", 200
Node.js
// Express example
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.MHS_WEBHOOK_SECRET;
app.post("/webhooks/mhs", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const given = req.get("X-MHS-Signature") || "";
if (given.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
console.log(event.event, event.data.id, event.data.reference);
res.sendStatus(200);
});
PHP
<?php
$secret = getenv('MHS_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
$given = $_SERVER['HTTP_X_MHS_SIGNATURE'] ?? '';
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
$order = $event['data'];
error_log("{$event['event']} order {$order['id']} ({$order['reference']})");
http_response_code(200);
Order object
| Field | Type | Description |
|---|---|---|
id | integer | Our order id. |
reference | string or null | Your reference, if you sent one. |
status | string | pending → processing → complete, or cancelled (refunded). |
network, network_name | string | Network code and display name. |
package_id, size_gb | integer, number | The bundle bought. |
phone_number | string | Normalised 10-digit recipient number. |
price, currency | string | Amount charged to your wallet. |
balance_after | string or null | Your wallet balance right after the charge. |
created_at, updated_at | string | ISO 8601 timestamps. |