Recharge, status and balance — over plain JSON.
Three POST endpoints and one outbound webhook. Authenticate with an IP-locked key, submit a request, then let the webhook tell you the outcome instead of polling for it.
No SOAP, no XML, no SDK to install. Anything that can POST can integrate.
A key only works from the server addresses you whitelist, so a leaked key on another machine is useless.
Register one URL and we push every outcome to you, signed, with automatic retries.
Authentication
Every request needs three things: your API user and key in the POST body, and a fixed band-key header.
Required header
| Header | Value | Notes |
|---|---|---|
| band-key | flexiloadapi.com | Fixed value. Send it on every call, unchanged. |
Service and operator codes
| Operator | Code | Operator | Code |
|---|---|---|---|
| Grameenphone | GP | Banglalink | BL |
| Robi | RB | Teletalk | TT |
| Airtel | AT | Skitto | SK |
Service codes vary by account — 64 is standard mobile recharge.
The full list for your account is shown in the dashboard.
1. Recharge request
Body parameters
| Parameter | Example | Description |
|---|---|---|
| userreq | 01743739873 | Your username |
| keyreq | xxxxxxxxxxxxxxxx | Your API key |
| numberreq | 01743739873 | Mobile number to recharge |
| amountreq | 10 | Recharge amount |
| servicereq | 64 | Service code |
| typereq | 1 | 1 = prepaid, 2 = postpaid |
| operatorreq | GP | GP, RB, AT, BL, TT or SK |
| idreq | BD030823122153128 | Your unique reference. Generate it yourself and keep it — it is how you look the transaction up later, and how the webhook is matched back to your order. |
Response
// accepted {"success": true, "status": 1, "message": "Flexiload-Request Submit Successfully"} // rejected {"success": false, "status": 2, "message": "Not User"}
/sendapi/status.
Example
<?php $url = 'https://flexiloadapi.com/sendapi/request'; $post = [ 'user' => '01743739873', 'key' => 'xxxxxxxxxxxxxxxx', 'number' => '01743739873', 'amount' => '10', 'service' => '64', 'type' => '1', 'operator' => 'GP', // your own reference — keep it against your order 'id' => 'BD' . date('dmyHis') . mt_rand(100, 999), ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['band-key: flexiloadapi.com'], CURLOPT_POST => true, CURLOPT_POSTFIELDS => $post, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $res = json_decode(curl_exec($ch), true); curl_close($ch); if (!empty($res['success'])) { // accepted — store $post['id'] and wait for the webhook }
// Node 18+, no dependencies const body = new URLSearchParams({ user: '01743739873', key: 'xxxxxxxxxxxxxxxx', number: '01743739873', amount: '10', service: '64', type: '1', operator: 'GP', id: 'BD' + Date.now() }); const res = await fetch('https://flexiloadapi.com/sendapi/request', { method: 'POST', headers: { 'band-key': 'flexiloadapi.com' }, body }); const data = await res.json(); console.log(data);
import requests, time r = requests.post( 'https://flexiloadapi.com/sendapi/request', headers={'band-key': 'flexiloadapi.com'}, data={ 'user': '01743739873', 'key': 'xxxxxxxxxxxxxxxx', 'number': '01743739873', 'amount': '10', 'service': '64', 'type': '1', 'operator': 'GP', 'id': 'BD' + str(int(time.time() * 1000)), }, timeout=30, ) print(r.json())
curl -X POST 'https://flexiloadapi.com/sendapi/request' \ -H 'band-key: flexiloadapi.com' \ -d 'user=01743739873' \ -d 'key=xxxxxxxxxxxxxxxx' \ -d 'number=01743739873' \ -d 'amount=10' \ -d 'service=64' \ -d 'type=1' \ -d 'operator=GP' \ -d 'id=BD030823122153128'
2. Recharge status
Body parameters
| Parameter | Example | Description |
|---|---|---|
| userreq | 01743739873 | Your username |
| keyreq | xxxxxxxxxxxxxxxx | Your API key |
| idreq | BD030823122153128 | The same reference you sent with the recharge request |
Response
{
"posotion": 1,
"trxid": "BD030823122153128",
"lastbalance": "102",
"status": "1"
}
status: 0 pending · 1 success · 2 failed ·
3 cancelled · 4 processing · 5 waiting.
The key is spelled posotion — that is not a typo in this page,
it is the field name the API returns.
3. Balance query
Body parameters
| Parameter | Example | Description |
|---|---|---|
| userreq | 01743739873 | Your username |
| keyreq | xxxxxxxxxxxxxxxx | Your API key |
Response
{"success": true, "status": 1, "balance": "102"}
Example
curl -X POST 'https://flexiloadapi.com/sendapi/balance' \ -H 'band-key: flexiloadapi.com' \ -d 'user=01743739873' \ -d 'key=xxxxxxxxxxxxxxxx'
Webhooks — why they matter
A recharge is not instant. You submit it, we queue it, the operator processes it, and somewhere between two seconds and two minutes later it succeeds or fails. The question is how your system finds out.
Polling the status endpoint
Ask every 5 seconds for 2 minutes and that is 24 requests per transaction, of which 23 tell you nothing new. At a thousand recharges a day that is 24,000 wasted calls. Your customer still waits up to 5 seconds after it completes, and if your cron stalls the order sits unresolved.
Taking the webhook
One request, at the moment it happens. Your customer sees the result immediately, your server does no idle work, and a failed delivery is retried automatically rather than lost. Nothing to schedule and nothing to monitor.
- You post a recharge Include your own
id. Store it against your order and return "processing" to your customer. - We work the request Queued, routed to the operator, confirmed or rejected.
- We POST to your URL The moment the outcome is known — signed, as JSON.
- You reply 200 Update the order and answer with any 2xx. That is the whole contract.
Setting one up
- Open API Settings in your dashboard The same page where your API key and whitelist live.
- Enter your callback URL Must be
https://and reachable from the public internet. Private or internal addresses are rejected. - Choose which events you want Successful, failed, cancelled — any combination.
- Copy your signing secret Generated for you. Keep it server-side, never in front-end code.
- Press Send a test delivery Fires a
pingevent at your endpoint so you can confirm it answers before relying on it.
What you receive
Headers we send
| Header | Example | Description |
|---|---|---|
| Content-Type | application/json | Body is raw JSON, not form fields |
| X-MITLoad-Signature | sha256=9f86d0818... | HMAC-SHA256 of the exact body, keyed with your secret |
| X-MITLoad-Delivery | whk_65b2f1a9c3e04 | Unique per attempt. Retries of the same event carry different values. |
Body
{
"event": "transaction.success",
"id": 920184,
"request_id": "BD030823122153128",
"transaction_id": "TRX8891042",
"number": "01743739873",
"amount": 100.00,
"cost": 98.40,
"service": "64",
"operator": "GP",
"pcode": "017",
"type": "1",
"status": 1,
"status_text": "success",
"sender_no": null,
"remark": "",
"requested_at": "2026-08-04 13:20:11",
"settled_at": "2026-08-04 13:20:48",
"sent_at": "2026-08-04T13:20:49+06:00"
}
| Field | Meaning |
|---|---|
| event | transaction.success, transaction.failed,
transaction.cancelled, or ping for a test |
| transaction_id | The id you sent with the original request —
match on this |
| amount | What the customer was recharged |
| cost | What it cost you, after your level commission |
| status | 1 success, 2 failed, 3 cancelled |
| settled_at | When the operator confirmed. Null if never settled. |
transaction.success and
your system would mark an unpaid order as delivered.
Verifying the signature
Compute HMAC-SHA256 over the raw request body using your secret,
and compare it with the header. Use a timing-safe comparison —
hash_equals, crypto.timingSafeEqual,
hmac.compare_digest — not ==.
Read the body before any framework parses it. Re-encoding parsed JSON changes the bytes and the signature will never match.
<?php $secret = 'your_signing_secret'; // the RAW body — not $_POST, not a re-encoded array $body = file_get_contents('php://input'); $sent = $_SERVER['HTTP_X_MITLOAD_SIGNATURE'] ?? ''; $mine = 'sha256=' . hash_hmac('sha256', $body, $secret); if (!hash_equals($mine, $sent)) { http_response_code(401); exit('bad signature'); } $e = json_decode($body, true); // idempotent: ignore an id you have already settled if (order_already_settled($e['request_id'])) { http_response_code(200); exit('ok'); } if ($e['status'] == 1) { mark_delivered($e['request_id'], $e['amount']); } else { refund_customer($e['request_id']); } http_response_code(200); echo 'ok';
const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.MITLOAD_SECRET; // express.raw, NOT express.json — we need the exact bytes app.post('/mitload/callback', express.raw({ type: 'application/json' }), (req, res) => { const mine = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex'); const sent = req.get('X-MITLoad-Signature') || ''; const a = Buffer.from(mine), b = Buffer.from(sent); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).send('bad signature'); } const e = JSON.parse(req.body.toString()); // answer fast, do the slow work afterwards res.status(200).send('ok'); settleOrder(e.request_id, e.status, e.amount).catch(console.error); });
import hmac, hashlib, os from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ['MITLOAD_SECRET'].encode() @app.route('/mitload/callback', methods=['POST']) def callback(): body = request.get_data() # raw bytes sent = request.headers.get('X-MITLoad-Signature', '') mine = 'sha256=' + hmac.new(SECRET, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(mine, sent): abort(401) e = request.get_json() if already_settled(e['request_id']): return 'ok', 200 if e['status'] == 1: mark_delivered(e['request_id'], e['amount']) else: refund(e['request_id']) return 'ok', 200
// routes/api.php Route::post('/mitload/callback', [MitloadController::class, 'handle']) ->withoutMiddleware([VerifyCsrfToken::class]); // app/Http/Controllers/MitloadController.php public function handle(Request $request) { $body = $request->getContent(); $mine = 'sha256=' . hash_hmac('sha256', $body, config('services.mitload.secret')); if (!hash_equals($mine, $request->header('X-MITLoad-Signature', ''))) { abort(401); } $e = json_decode($body, true); Order::where('reference', $e['request_id']) ->where('status', 'pending') ->update(['status' => $e['status'] == 1 ? 'delivered' : 'failed']); return response('ok', 200); }
withoutMiddleware in Laravel,
csrf_exempt in Django, or the exclusion list in CodeIgniter.
A 403 on every delivery is almost always this.
Retries and idempotency
What counts as delivered
Any 2xx response. Anything else — 4xx, 5xx, a timeout, a TLS failure — is a failure and will be retried.
Retry schedule
| Attempt | Sent after |
|---|---|
| 1 | immediately |
| 2 | 1 minute later |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 3 hours |
| 7 | 6 hours, then we stop |
Make your handler idempotent
A retry means you may see the same event twice — for instance if your 200 was
lost on the way back to us. Key on request_id and ignore
anything you have already settled. Without that, a retry could credit a
customer twice.
A complete receiver
Signature check, idempotency and a fast reply, in about forty lines.
<?php // callback.php — point your webhook URL here $secret = getenv('MITLOAD_SECRET'); $body = file_get_contents('php://input'); $sent = $_SERVER['HTTP_X_MITLOAD_SIGNATURE'] ?? ''; // 1. prove it came from MITLoad if (!hash_equals('sha256=' . hash_hmac('sha256', $body, $secret), $sent)) { http_response_code(401); exit; } $e = json_decode($body, true); // 2. a test ping needs nothing but a 200 if (($e['event'] ?? '') === 'ping') { http_response_code(200); exit('pong'); } $pdo = new PDO('mysql:host=localhost;dbname=shop', $u, $p); // 3. idempotent — the UPDATE only matches a row still pending, // so a duplicate delivery changes nothing $st = $pdo->prepare( 'UPDATE orders SET status = ?, settled_at = ? WHERE reference = ? AND status = "pending"' ); $st->execute([ $e['status'] == 1 ? 'delivered' : 'failed', $e['settled_at'], $e['request_id'], ]); // 4. answer immediately; anything slow goes on a queue http_response_code(200); echo 'ok'; if ($st->rowCount() > 0 && $e['status'] == 1) { queue_customer_sms($e['number'], $e['amount']); }
Not for the normal path. It is worth keeping as a reconciliation sweep — once every ten minutes, ask about anything still pending on your side. That covers the rare case where your server was down for the whole retry window.
Almost always CSRF protection. We are a server, not a browser, and send no CSRF token. Exempt the callback route in your framework.
You are almost certainly hashing a re-encoded body rather than the raw bytes. Capture the body before any JSON middleware parses it. Key order and whitespace both change the hash.
No. The URL must be https and publicly reachable — private and internal addresses are rejected when you save it. For local development use a tunnel that gives you a public https address.
We retry seven times over about ten hours, then stop. Deliveries that gave up stay in our log, and anything you missed can be recovered from the status endpoint.
Not currently — one URL per account. Switch on the event types you want and branch on the event field in your handler.
Get your API key
Register, verify, generate a key and whitelist your server. Usually done the same day.