Developer API · v1

Mall House Services API documentation

Sell data bundles from your own website or app. Orders are paid from your Mall House Services wallet at your account's price.

Get an API key

Quick start

  1. Create an account, then open API keys in the dashboard and generate a key. Copy it immediately: it is shown only once.
  2. Fund your wallet. API orders are charged to it, exactly like orders on the website.
  3. Send requests to the base URL below with your key in the Authorization header.
BASEhttps://mallhouseservices.com/api/v1

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.

Header
Authorization: Bearer mhs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys 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
{
  "success": true,
  "data": { "...": "..." }
}
Error
{
  "success": false,
  "error": {
    "code": "insufficient_balance",
    "message": "Your wallet balance is too low for this order.",
    "balance": "3.00",
    "required": "25.00"
  }
}

Error codes

HTTPCodeMeaning
400validation_errorA field is missing or invalid. field names it.
400invalid_jsonThe body is not a JSON object.
400invalid_networkUnknown network code. valid lists the accepted codes.
401invalid_api_keyMissing, mistyped or revoked key.
402insufficient_balanceWallet too low. Top up and retry with the same reference.
403account_inactiveThe account behind the key is disabled.
404package_not_found / order_not_foundNo such active package, or no such order on your account.
405method_not_allowedWrong HTTP method for the endpoint.
429rate_limitedToo many requests this minute.
503shop_closedOutside business hours. message gives the hours.
503network_unavailableThat network is down or paused.
503api_disabledThe 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

GET/api/v1/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'];
Response
{
  "success": true,
  "data": {
    "username": "kofi_shop",
    "account_type": "agent",
    "balance": "248.50",
    "currency": "GHS"
  }
}

Networks

GET/api/v1/networks/

Lists every network with its live status and whether orders are being accepted. Network codes:

  • mtn: MTN
  • telecel: Telecel
  • airteltigo_ishare: AirtelTigo iShare
  • airteltigo_bigtime: AirtelTigo Big Time

Packages

GET/api/v1/packages/?network=mtn

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";
}
Response
{
  "success": true,
  "data": [
    { "id": 12, "network": "mtn", "network_name": "MTN",
      "size_gb": 5.0, "price": "26.13", "currency": "GHS" }
  ]
}

Place an order

POST/api/v1/orders/

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

FieldTypeRequired?Description
phone_numberstringRequiredRecipient number, e.g. 0241234567. +233 and 9-digit forms are accepted.
package_idintegerRequired unless network + size_gb are sentFrom List packages.
networkstringWith size_gbA network code such as mtn.
size_gbnumberWith networkBundle size in GB, e.g. 5.
referencestringOptional (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']}";
}
Response (201 Created)
{
  "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

GET/api/v1/orders/?status=pending&page=1&per_page=25

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

GET/api/v1/orders/{id}/

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 2xx within 8 seconds. Deliveries are not retried, so poll Get an order as a fallback.
Webhook body
{
  "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

FieldTypeDescription
idintegerOur order id.
referencestring or nullYour reference, if you sent one.
statusstringpending → processing → complete, or cancelled (refunded).
network, network_namestringNetwork code and display name.
package_id, size_gbinteger, numberThe bundle bought.
phone_numberstringNormalised 10-digit recipient number.
price, currencystringAmount charged to your wallet.
balance_afterstring or nullYour wallet balance right after the charge.
created_at, updated_atstringISO 8601 timestamps.