Recharge API

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.

REST and JSON

No SOAP, no XML, no SDK to install. Anything that can POST can integrate.

IP-locked keys

A key only works from the server addresses you whitelist, so a leaked key on another machine is useless.

Webhooks, not polling

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

HeaderValueNotes
band-keyflexiloadapi.com Fixed value. Send it on every call, unchanged.
Your key only works from whitelisted addresses. Add every server that will call the API — including staging — under API Settings in your dashboard. Requests from anywhere else are rejected even with a valid key.

Service and operator codes

OperatorCodeOperatorCode
GrameenphoneGPBanglalinkBL
RobiRBTeletalkTT
AirtelATSkittoSK

Service codes vary by account — 64 is standard mobile recharge. The full list for your account is shown in the dashboard.

1. Recharge request

POST https://flexiloadapi.com/sendapi/request

Body parameters

ParameterExampleDescription
userreq01743739873Your username
keyreqxxxxxxxxxxxxxxxxYour API key
numberreq01743739873Mobile number to recharge
amountreq10Recharge amount
servicereq64Service code
typereq11 = prepaid, 2 = postpaid
operatorreqGPGP, RB, AT, BL, TT or SK
idreqBD030823122153128 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"}
A success here means accepted, not delivered. The operator has not been contacted yet. The final outcome arrives by webhook, or you can poll /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

POST https://flexiloadapi.com/sendapi/status

Body parameters

ParameterExampleDescription
userreq01743739873Your username
keyreqxxxxxxxxxxxxxxxxYour API key
idreqBD030823122153128 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.

Do not poll this in a tight loop. If you find yourself calling it every few seconds, use a webhook instead — it is the same information, delivered the moment it changes, at no cost to either side.

3. Balance query

POST https://flexiloadapi.com/sendapi/balance

Body parameters

ParameterExampleDescription
userreq01743739873Your username
keyreqxxxxxxxxxxxxxxxxYour 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.
You can use both. Webhooks for the normal path, and the status endpoint as a reconciliation sweep for anything still open after ten minutes. That combination survives an outage on either side.

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 ping event at your endpoint so you can confirm it answers before relying on it.

What you receive

POST your callback URL

Headers we send

HeaderExampleDescription
Content-Typeapplication/jsonBody is raw JSON, not form fields
X-MITLoad-Signaturesha256=9f86d0818... HMAC-SHA256 of the exact body, keyed with your secret
X-MITLoad-Deliverywhk_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"
}
FieldMeaning
eventtransaction.success, transaction.failed, transaction.cancelled, or ping for a test
transaction_idThe id you sent with the original request — match on this
amountWhat the customer was recharged
costWhat it cost you, after your level commission
status1 success, 2 failed, 3 cancelled
settled_atWhen the operator confirmed. Null if never settled.
Never act on an unverified webhook. Your callback URL is reachable by anyone who guesses it. Without checking the signature, a stranger could POST a fake 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);
}
CSRF protection will block us. We are a server, not a browser, and carry no CSRF token. Exempt your callback route — 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

AttemptSent after
1immediately
21 minute later
35 minutes
415 minutes
51 hour
63 hours
76 hours, then we stop
Answer quickly, work afterwards. We wait 15 seconds. If your handler sends an SMS, writes to a slow database or calls another API before replying, you will hit that limit and get retried for something that actually succeeded. Reply 200 first, then process.

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 started

Get your API key

Register, verify, generate a key and whitelist your server. Usually done the same day.

WhatsApp