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.

Base URL: https://vendorsapps.com/docs/api

Overview

The Vendor Apps API lets agent accounts place bundle and airtime orders programmatically. When an order is placed:

Admin Setup (Required)

Before agents can use the API, the admin must complete these steps:

1

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.

2

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

3

Approve agents

Go to Admin → Users and approve users as agents. Only agent accounts can use the API.

4

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.

5

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.

6

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
Keep your API key secret. Anyone with your key can place orders and deduct from your wallet. Revoke compromised keys immediately from Profile → API Keys.

Error Handling

All responses are JSON. Successful responses have "success": true. Errors have "success": false and an "error" string.

HTTP StatusMeaning
200Success
401Missing or invalid API key
402Insufficient wallet balance
403Account not an active agent
404Bundle or order not found
422Missing or invalid request field
500Server error
// Error response example
{
  "success": false,
  "error": "Insufficient wallet balance.",
  "balance": 12.50,
  "required": 20.00,
  "shortfall": 7.50
}

List Bundles

GET /api/list_bundles.php Returns your available bundles

Query Parameters

ParameterTypeRequiredDescription
api_keystringrequired*Your API key (or use X-API-KEY header)
networkstringoptionalFilter 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

POST /api/order_bundle.php Place a data bundle order

Request Body (JSON or form-encoded)

FieldTypeRequiredDescription
bundle_idintrequiredThe bundle_id from list_bundles
customer_phonestringrequiredRecipient phone. Accepts 0XXXXXXXXX, +233XXXXXXXXX, or 233XXXXXXXXX
customer_namestringoptionalCustomer'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-..."
}
Routing: If your bundle came from the platform catalog → admin processes it. If from a super-agent → that agent processes it. If custom → you process it yourself.

Order Airtime

POST /api/order_airtime.php Place an airtime order

Request Body

FieldTypeRequiredDescription
customer_phonestringrequiredRecipient phone number
networkstringrequiredMTN, Telecel, or AirtelTigo
amountfloatrequiredAirtime value in GHS. Minimum: 1.00
customer_namestringoptionalCustomer'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."
}
Commission: The admin sets the commission % in Settings. You keep that percentage — only the net amount is deducted from your wallet. The admin processes and sends the full airtime amount to the customer.

Order Status

GET /api/order_status.php Check the status of any order

Query Parameters

ParameterTypeRequiredDescription
refstringrequiredThe reference returned when the order was placed
api_keystringrequired*Your API key (or X-API-KEY header)

Status Values

StatusMeaning
processingOrder received, waiting to be fulfilled
completedBundle/airtime sent to customer
failedOrder could not be fulfilled
refundedOrder cancelled and wallet refunded

The Payment / Checkout system has two integration modes. Choose the one that fits your use case:

ModeHow it worksAuth 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
Where to get your slug & API key: Your slug is shown in the Collect Payment dashboard (your POS URL is /p/{slug}, payment links use /pay/{slug}). Your API key is generated at Profile → API Keys.

API — Initiate a Charge

POST /api/pos_charge.php Send a MoMo payment prompt to a customer
Authentication: Pass your API key via the 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)

FieldTypeRequiredDescription
api_keystringrequired*Your API key — or use X-API-KEY header instead
customer_phonestringrequiredCustomer's MoMo number. Accepts 0XXXXXXXXX, +233XXXXXXXXX, or 233XXXXXXXXX
amountfloatrequiredAmount in GHS before the platform fee (e.g. 50.00). The fee is added on top — customer is charged amount × (1 + fee%).
descriptionstringoptionalWhat the payment is for (e.g. "Order #1042"). Shown to the customer in the MoMo prompt.
customer_namestringoptionalCustomer name for your records
refstringoptionalYour 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."
}
Prompt is async: A successful response means the MoMo prompt was sent — not that the customer has approved. Poll /api/pos_status.php?ref=... to detect when they approve.

API — Check Charge Status

GET /api/pos_status.php Check if a charge has been approved

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

ParameterTypeRequiredDescription
refstringrequiredThe 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://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://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://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";
Reconciliation tip: Pass your own order ID as 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

GET /pay/{slug} Hosted checkout page
ParameterTypeRequiredDescription
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://vendorsapps.com/docs/pay/abc12345?amount=50&description=Invoice+%231042

// Open amount — customer types their own amount
https://vendorsapps.com/docs/pay/abc12345?description=Deposit

// With your own order reference for reconciliation
https://vendorsapps.com/docs/pay/abc12345?amount=120&description=Order+%239&ref=ORD-9
Platform fee: A percentage fee (set by admin) is added on top of the amount passed. The customer sees the final total before they approve. You receive the base amount net of the fee.

How it works

1

Customer opens the link

A clean checkout page loads — your business name, the amount, and a phone number field. No login required.

2

Customer enters their MoMo number

They type their Mobile Money number. A payment prompt is sent to their phone via Moolre.

3

Customer approves on their phone

The checkout page polls for confirmation. On approval, a success screen is shown automatically.

4

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://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://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://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://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>
No webhook needed for basic use. The checkout page handles the full flow internally. For server-side confirmation (e.g. to mark an order as paid in your own database), store the 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://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://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://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://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://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).

Vendor Apps API Documentation · Back to Platform