API Documentation
Integrate Vendor Apps into your own platform to sell data bundles and airtime. Orders are deducted directly from your wallet — no payment gateway needed on your end.
https://www.vendorsapps.com/docs/api
Overview
The Vendor Apps API lets agent accounts place bundle and airtime orders programmatically. When an order is placed:
- The cost is deducted from your wallet immediately
- The order is created and routed to the right processor (admin, super-agent, or yourself)
- If the order fails to save, your wallet is automatically refunded
- You can check order status at any time using the reference returned
Admin Setup (Required)
Before agents can use the API, the admin must complete these steps:
Add bundles to the platform catalog
Go to Admin → Bundles and add the data bundles you want to offer. Set the cost price and platform price for each bundle.
Set the airtime commission
Go to Admin → Settings → Fees & Limits and set airtime_commission_pct. This is the percentage agents keep on airtime orders (e.g. 2% on GHS 50 = GHS 1 commission, agent pays GHS 49).
Approve agents
Go to Admin → Users and approve users as agents. Only agent accounts can use the API.
Agent: sync bundles to their store
The agent logs in, goes to Data Bundles → Platform Catalog and clicks Sync Platform Bundles. This adds the admin's bundles to their store with their own pricing. The bundle_id values returned by list_bundles come from this store.
Agent: generate an API key
The agent goes to Profile → API Keys and clicks Generate New Key. Copy the key immediately — it won't be shown again in full.
Agent: top up wallet
The agent must have sufficient wallet balance. Orders are deducted at the time of placement. Top up at Wallet → Deposit.
Authentication
All API requests require an API key. Pass it in one of two ways:
Option A — HTTP Header (recommended)
X-API-KEY: bh_your_api_key_here
Option B — Request body
api_key=bh_your_api_key_here
Error Handling
All responses are JSON. Successful responses have "success": true. Errors have "success": false and an "error" string.
| HTTP Status | Meaning |
|---|---|
| 200 | Success |
| 401 | Missing or invalid API key |
| 402 | Insufficient wallet balance |
| 403 | Account not an active agent |
| 404 | Bundle or order not found |
| 422 | Missing or invalid request field |
| 500 | Server error |
// Error response example
{
"success": false,
"error": "Insufficient wallet balance.",
"balance": 12.50,
"required": 20.00,
"shortfall": 7.50
}
List Bundles
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
api_key | string | required* | Your API key (or use X-API-KEY header) |
network | string | optional | Filter by network: MTN, Telecel, AirtelTigo |
Response
{
"success": true,
"count": 3,
"bundles": [
{
"bundle_id": 4, // use this as bundle_id when ordering
"name": "2GB MTN",
"network": "MTN",
"data_size": "2GB",
"data_mb": 2048,
"validity_days": 30,
"price": 9.00, // GHS — deducted from your wallet
"stock": "unlimited",
"source": "platform" // platform | super_agent | custom
}
]
}
Order Bundle
Request Body (JSON or form-encoded)
| Field | Type | Required | Description |
|---|---|---|---|
bundle_id | int | required | The bundle_id from list_bundles |
customer_phone | string | required | Recipient phone. Accepts 0XXXXXXXXX, +233XXXXXXXXX, or 233XXXXXXXXX |
customer_name | string | optional | Customer's name for your records |
Response
{
"success": true,
"order_id": 42,
"reference": "BAPI-A1B2C3D4-20250517",
"bundle_name": "2GB MTN",
"network": "MTN",
"data": "2GB",
"validity": "30 days",
"customer": "0241234567",
"amount_paid": 9.00,
"status": "processing",
"routed_to": "admin", // admin | super_agent | self
"message": "Order placed successfully. Check status at GET /api/order_status.php?ref=BAPI-..."
}
Order Airtime
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
customer_phone | string | required | Recipient phone number |
network | string | required | MTN, Telecel, or AirtelTigo |
amount | float | required | Airtime value in GHS. Minimum: 1.00 |
customer_name | string | optional | Customer's name |
Response
{
"success": true,
"order_id": 15,
"reference": "AAPI-E5F6G7H8-20250517",
"network": "MTN",
"customer": "0241234567",
"amount": 50.00,
"commission": 1.00, // GHS you keep (2% of 50)
"deducted": 49.00, // GHS deducted from your wallet
"status": "processing",
"routed_to": "admin",
"message": "Order placed. Admin will process and send airtime."
}
Order Status
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
ref | string | required | The reference returned when the order was placed |
api_key | string | required* | Your API key (or X-API-KEY header) |
Status Values
| Status | Meaning |
|---|---|
processing | Order received, waiting to be fulfilled |
completed | Bundle/airtime sent to customer |
failed | Order could not be fulfilled |
refunded | Order cancelled and wallet refunded |
Payment Links & Checkout API
The Payment / Checkout system has two integration modes. Choose the one that fits your use case:
| Mode | How it works | Auth required? | Best for |
|---|---|---|---|
| Hosted link | Share a URL — customer opens it, enters their phone, approves MoMo prompt | None (slug-based) | Invoices, email links, buttons on a website, physical signage |
| API charge | POST from your server with the customer's phone and amount — MoMo prompt is sent, you poll for completion | API key required | Custom checkouts, apps, automations, server-to-server flows |
/p/{slug}, payment links use /pay/{slug}).
Your API key is generated at Profile → API Keys.
API — Initiate a Charge
X-API-KEY header or the api_key field. No slug required — the system looks up your terminal automatically.
Request Body (JSON or form-encoded)
| Field | Type | Required | Description |
|---|---|---|---|
api_key | string | required* | Your API key — or use X-API-KEY header instead |
customer_phone | string | required | Customer's MoMo number. Accepts 0XXXXXXXXX, +233XXXXXXXXX, or 233XXXXXXXXX |
amount | float | required | Amount in GHS before the platform fee (e.g. 50.00). The fee is added on top — customer is charged amount × (1 + fee%). |
description | string | optional | What the payment is for (e.g. "Order #1042"). Shown to the customer in the MoMo prompt. |
customer_name | string | optional | Customer name for your records |
ref | string | optional | Your own external order reference for reconciliation |
Response
{
"success": true,
"paid": false, // true only in test/sandbox mode — always false in live
"ref": "POS-XXXXXXXX-20250609", // use this to poll for status
"sale_number": "POS-20250609-AB12CD",
"total": 51.00, // amount charged to customer (with fee)
"net": 50.00, // amount credited to your wallet on payment
"message": "Payment prompt sent to 0241234567."
}
/api/pos_status.php?ref=... to detect when they approve.
API — Check Charge Status
This endpoint is public — no API key needed. It is intentionally open so the hosted checkout page can poll without exposing your key to the browser. Reference IDs are non-guessable.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
ref | string | required | The ref returned by pos_charge.php |
Response
// Pending (customer hasn't approved yet)
{ "paid": false, "status": "pending" }
// Approved — wallet credited
{ "paid": true, "status": "completed" }
// Declined / timed out
{ "paid": false, "status": "failed" }
API Charge Flow
Your Server Vendor Apps API Customer Phone
| | |
|-- POST /api/pos_charge.php --| |
| X-API-KEY: bh_your_key | |
| amount: 50, phone: 024... | |
| |-- Validate key |
| |-- Create sale record |
|<-- { success, ref, total } --|-- Send MoMo prompt ->|
| | |-- Customer approves
|-- GET /api/pos_status.php --- | |
| ?ref=POS-XXXXX |<-- Webhook confirms -|
|<-- { paid: false, status: | |
| "pending" } | |
| | |
|-- GET /api/pos_status.php ---| |
|<-- { paid: true, status: | |
| "completed" } | |
| | |
| (wallet credited, sale | |
| appears in POS history) | |
cURL example
curl -X POST \
'https://www.vendorsapps.com/docs/api/pos_charge.php' \
-H 'X-API-KEY: bh_your_key_here' \
-H 'Content-Type: application/json' \
-d '{
"customer_phone": "0241234567",
"amount": 50.00,
"description": "Order #1042",
"customer_name": "John Doe",
"ref": "ORD-1042"
}'
JavaScript (server-side / Node.js)
const API_KEY = 'bh_your_key_here';
const BASE = 'https://www.vendorsapps.com/docs';
async function chargeCustomer(phone, amount, description, yourRef) {
// 1. Initiate charge
const charge = await fetch(`${BASE}/api/pos_charge.php`, {
method: 'POST',
headers: { 'X-API-KEY': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_phone: phone,
amount: amount,
description: description,
ref: yourRef,
}),
}).then(r => r.json());
if (!charge.success) throw new Error(charge.error);
console.log('Prompt sent. Ref:', charge.ref, 'Total charged:', charge.total);
// 2. Poll until paid (max ~2 min)
for (let i = 0; i < 40; i++) {
await new Promise(r => setTimeout(r, 3000)); // wait 3 s
const status = await fetch(`${BASE}/api/pos_status.php?ref=${encodeURIComponent(charge.ref)}`)
.then(r => r.json());
if (status.paid) return { paid: true, ref: charge.ref };
if (status.status === 'failed') return { paid: false, ref: charge.ref, reason: 'declined' };
}
return { paid: false, ref: charge.ref, reason: 'timeout' };
}
// Usage
chargeCustomer('0241234567', 50, 'Order #1042', 'ORD-1042').then(console.log);
PHP
<?php
$apiKey = 'bh_your_key_here';
$base = 'https://www.vendorsapps.com/docs';
function posCharge(string $phone, float $amount, string $description, string $yourRef, string $key, string $base): array {
$ch = curl_init("$base/api/pos_charge.php");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['X-API-KEY: '.$key, 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'customer_phone' => $phone,
'amount' => $amount,
'description' => $description,
'ref' => $yourRef,
]),
]);
$res = curl_exec($ch); curl_close($ch);
return json_decode($res, true) ?? [];
}
function pollStatus(string $ref, string $base, int $maxTries = 40, int $intervalSec = 3): string {
for ($i = 0; $i < $maxTries; $i++) {
sleep($intervalSec);
$r = json_decode(file_get_contents("$base/api/pos_status.php?ref=".urlencode($ref)), true);
if ($r['paid'] ?? false) return 'completed';
if (($r['status'] ?? '') === 'failed') return 'failed';
}
return 'timeout';
}
$charge = posCharge('0241234567', 50.00, 'Order #1042', 'ORD-1042', $apiKey, $base);
if (!$charge['success']) { die('Error: '.$charge['error']); }
echo "Prompt sent. Ref: {$charge['ref']}\n";
$outcome = pollStatus($charge['ref'], $base);
echo "Outcome: {$outcome}\n";
ref when charging. It is stored against the sale record so you can match it in your own database when the payment completes.
Hosted Payment Link — URL Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slug |
string | required | Your unique terminal slug from the Collect Payment dashboard. Part of the URL path, not a query param. |
amount |
float | optional | Amount in GHS. If provided, the amount field is locked — the customer cannot change it. If omitted, the customer enters their own amount. |
description |
string | optional | Short text shown on the checkout page describing what the payment is for (e.g. "Order #1042", "Monthly subscription"). |
ref |
string | optional | Your own external order or transaction reference. Stored with the sale for reconciliation — does not affect the flow. |
Examples
// Fixed amount — customer pays exactly GHS 50, no editing
https://www.vendorsapps.com/docs/pay/abc12345?amount=50&description=Invoice+%231042
// Open amount — customer types their own amount
https://www.vendorsapps.com/docs/pay/abc12345?description=Deposit
// With your own order reference for reconciliation
https://www.vendorsapps.com/docs/pay/abc12345?amount=120&description=Order+%239&ref=ORD-9
How it works
Customer opens the link
A clean checkout page loads — your business name, the amount, and a phone number field. No login required.
Customer enters their MoMo number
They type their Mobile Money number. A payment prompt is sent to their phone via Moolre.
Customer approves on their phone
The checkout page polls for confirmation. On approval, a success screen is shown automatically.
Your wallet is credited
The net amount (after platform fee) is added to your Vendor Apps wallet. The sale appears in your POS history.
Embed & Redirect
Simple anchor link
The easiest integration — just link to the checkout page.
<a href="https://www.vendorsapps.com/docs/pay/YOUR_SLUG?amount=50&description=My+Product&ref=ORD-1"
target="_blank"
rel="noopener">
Pay GHS 50.00
</a>
Checkout button
<!-- Styled pay button pointing to your checkout page -->
<a href="https://www.vendorsapps.com/docs/pay/YOUR_SLUG?amount=50&description=My+Product&ref=ORD-1"
target="_blank"
style="display:inline-block;padding:12px 28px;background:#22c55e;color:#fff;
border-radius:8px;font-weight:700;text-decoration:none;font-family:sans-serif;">
Pay GHS 50.00 via MoMo
</a>
Inline iframe embed
Embed the checkout page directly inside your site. The modal is designed to fit within a small frame.
<iframe
src="https://www.vendorsapps.com/docs/pay/YOUR_SLUG?amount=50&description=My+Product"
width="480"
height="600"
style="border:none;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);"
title="Checkout">
</iframe>
JavaScript Popup
Open the checkout in a centered popup window — closest to an in-page modal without any SDK required.
<script>
/**
* Open the checkout in a centered popup window.
* @param {string} slug - Your POS slug
* @param {number} amount - Amount in GHS (0 = open / customer enters)
* @param {string} description - What the payment is for
* @param {string} ref - Your own order reference (optional)
*/
function openCheckout(slug, amount, description, ref) {
const base = 'https://www.vendorsapps.com/docs';
let url = base + '/pay/' + slug;
const params = new URLSearchParams();
if (amount > 0) params.set('amount', amount);
if (description) params.set('description', description);
if (ref) params.set('ref', ref);
if ([...params].length) url += '?' + params.toString();
const w = 480, h = 620;
const left = Math.max(0, (screen.width - w) / 2);
const top = Math.max(0, (screen.height - h) / 2);
window.open(url, 'checkout',
`width=${w},height=${h},top=${top},left=${left},resizable=yes,scrollbars=yes`);
}
</script>
<!-- Usage -->
<button onclick="openCheckout('YOUR_SLUG', 50, 'Order #1042', 'ORD-1042')">
Pay GHS 50.00
</button>
Dynamic amount (user fills in on your site first)
<input type="number" id="amt" placeholder="Enter amount" min="1" step="0.01">
<button onclick="
const a = parseFloat(document.getElementById('amt').value);
if (a > 0) openCheckout('YOUR_SLUG', a, 'Custom Payment', '');
else alert('Enter a valid amount');
">
Pay Now
</button>
ref you pass in and check your Vendor Apps POS sale history, or contact admin to set up a webhook callback.
Order Flow
Your App BulksHub API Processor
| | |
|-- POST /order_bundle ---->| |
| |-- Validate API key |
| |-- Check wallet balance |
| |-- Deduct wallet |
| |-- Create order |
| |-- Route to processor --->|
|<-- { success, ref } -------| |
| | (admin/super-agent
| | sends bundle)
|-- GET /order_status?ref=--| |
|<-- { status: "completed" }| |
Code Examples
cURL — List bundles
curl -X GET \
'https://www.vendorsapps.com/docs/api/list_bundles.php?network=MTN' \
-H 'X-API-KEY: bh_your_key_here'
cURL — Order a bundle
curl -X POST \
'https://www.vendorsapps.com/docs/api/order_bundle.php' \
-H 'X-API-KEY: bh_your_key_here' \
-H 'Content-Type: application/json' \
-d '{
"bundle_id": 4,
"customer_phone": "0241234567",
"customer_name": "John Doe"
}'
cURL — Order airtime
curl -X POST \
'https://www.vendorsapps.com/docs/api/order_airtime.php' \
-H 'X-API-KEY: bh_your_key_here' \
-H 'Content-Type: application/json' \
-d '{
"customer_phone": "0241234567",
"network": "MTN",
"amount": 20.00
}'
JavaScript (fetch)
const API_KEY = 'bh_your_key_here';
const BASE = 'https://www.vendorsapps.com/docs/api';
// 1. Get available bundles
const bundles = await fetch(`${BASE}/list_bundles.php`, {
headers: { 'X-API-KEY': API_KEY }
}).then(r => r.json());
// 2. Place an order
const order = await fetch(`${BASE}/order_bundle.php`, {
method: 'POST',
headers: { 'X-API-KEY': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
bundle_id: bundles.bundles[0].bundle_id,
customer_phone: '0241234567',
customer_name: 'John Doe'
})
}).then(r => r.json());
// 3. Check status
const status = await fetch(`${BASE}/order_status.php?ref=${order.reference}`, {
headers: { 'X-API-KEY': API_KEY }
}).then(r => r.json());
PHP
<?php
$apiKey = 'bh_your_key_here';
$base = 'https://www.vendorsapps.com/docs/api';
function apiCall(string $url, array $data = null, string $key): array {
$ch = curl_init($url);
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-KEY: '.$key, 'Content-Type: application/json'],
];
if ($data !== null) {
$opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($ch, $opts);
$res = curl_exec($ch);
curl_close($ch);
return json_decode($res, true) ?? [];
}
// List bundles
$bundles = apiCall("$base/list_bundles.php?network=MTN", null, $apiKey);
// Place order
$order = apiCall("$base/order_bundle.php", [
'bundle_id' => $bundles['bundles'][0]['bundle_id'],
'customer_phone' => '0241234567',
'customer_name' => 'John Doe',
], $apiKey);
echo $order['reference']; // BAPI-XXXXXXXX-YYYYMMDD
FAQ
Do I need to set up a payment gateway?
No. Orders are paid from your Vendor Apps wallet. You top up your wallet once (via mobile money on the platform), then use the API to place orders — no payment integration needed on your end.
What happens if an order fails after my wallet is deducted?
If the order record fails to save (database error), your wallet is automatically refunded immediately. If the order saves but the processor fails to deliver, contact admin — they can refund via the admin panel.
What is bundle_id?
It's the id from your agent_bundles store — not the master bundle catalog ID. Always use the bundle_id returned by list_bundles. These are specific to your account.
Can I use the API without being an agent?
No. Only approved agent accounts can generate API keys and place orders. Contact admin to get approved.
How do I know when an order is completed?
Poll GET /api/order_status.php?ref=YOUR_REF periodically. Orders typically complete within a few minutes once the processor acts on them.
What is the rate limit?
There is no hard rate limit currently, but excessive requests may be throttled. Use reasonable polling intervals (minimum 10 seconds for status checks).