Abliner API · v1
Build with Abliner
Start with one API key, one balance call, and one webhook URL. This page is written to help you integrate in minutes without guessing the response format.
Overview
Introduction
Abliner gives you a single integration to collect money (Mobile Money, Cards, QR), send money (mobile wallets & bank accounts), and send Bulk SMS from your approved Sender ID - all behind one REST API and one key.
Base URL
https://abliner.net/api/v1This is the production API. Usehttps://abliner.net/api/v1 in all examples below.
Conventions
- All requests and responses are
application/json; charset=utf-8. - Amounts are passed in whole TZS (not cents). Example:
1500= TSh 1,500. - Phone numbers may be sent as
0712345678or255712345678- we normalize. - All timestamps are ISO 8601 in UTC.
- Every successful response includes an
idyou can use for reconciliation.
Current Pricing (Abliner)
- Bulk SMS: TZS 28 / SMS (live price - may be tuned by admin and is returned by the API).
- Cash-Out (Disbursements): from 2%, depending on the Mobile Money network, and never more than 4%.
- Mobile Money deposits: free (0%).
- Card deposits (Visa / Mastercard): 4%.
- Deposit limits: minimum TZS 500, maximum TZS 3,000,000 per transaction.
5-minute setup
Quickstart
- Create an account and verify your phone number.
- Submit a Business Application if you want live production traffic.
- Open API Keys and generate a key. Copy it immediately.
- On the same page, generate your Webhook signing secret (starts with
whsec_). To know when a payment completes, pass your own HTTPScallback_urlin your API requests — see Webhooks below. - Run the balance call below first. If that works, the rest of the integration is just the same pattern.
ABLINER_API_KEY— the API key (starts withtsl_live_), sent asAuthorization: Bearer ...on every request.ABLINER_WEBHOOK_SECRET— the webhook signing secret (starts withwhsec_), generated on Dashboard > API Keys. It is used to sign every deposit request and to verifyx-webhook-signatureon incoming webhooks.
Every POST /api/v1/deposits must be signed (API keys created from 5 Oct 2026). Without a valid signature the request is rejected with 401 invalid_request_signature and no payment prompt is sent:
const body = JSON.stringify({ amount: 1000, method: "mobile", phone: "2557XXXXXXXX", currency: "TZS" });
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac("sha256", process.env.ABLINER_WEBHOOK_SECRET)
.update(ts + "." + body).digest("hex");
await fetch("https://abliner.net/api/v1/deposits", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.ABLINER_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": yourPaymentId,
"x-abliner-timestamp": ts,
"x-abliner-signature": sig,
},
body, // send exactly the same string you signed
});Also ask for the webhook endpoint URL and register it on Dashboard > API Keys. Never skip the signature verification step — see the Webhooks section below for the exact verification code.
/api/v1/balance to confirm your key is valid, then use /api/v1/deposits to collect and /api/v1/withdrawals to disburse real flows.Copy-ready Mobile Money push
Replace only the API key, amount, phone and reference. A successful request immediately returns 201 with a transaction ID while the customer confirms on their phone.
curl -X POST https://abliner.net/api/v1/deposits \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 1000,
"method": "mobile",
"currency": "TZS",
"phone": "255712345678",
"reference": "ORDER-1001",
"callback_url": "https://yourapp.com/webhooks/abliner"
}'cURL
curl https://abliner.net/api/v1/balance \
-H "Authorization: Bearer tsl_live_xxxxxxxxxxxx"Node.js (fetch)
const BASE_URL = process.env.ABLINER_API_BASE_URL ?? "https://abliner.net";
const res = await fetch(`${BASE_URL}/api/v1/balance`, {
headers: { Authorization: `Bearer ${process.env.ABLINER_API_KEY}` },
});
const body = await res.json();
console.log(body);
// body.status === "success"
// body.data.balance === 1250
// body.data.currency === "TZS"PHP
<?php
$baseUrl = getenv("ABLINER_API_BASE_URL") ?: "https://abliner.net";
$ch = curl_init($baseUrl . "/api/v1/balance");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . getenv("ABLINER_API_KEY"),
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
print_r($data);Python
import os, requests
BASE_URL = os.environ.get("ABLINER_API_BASE_URL", "https://abliner.net")
res = requests.get(
f"{BASE_URL}/api/v1/balance",
headers={"Authorization": f"Bearer {os.environ['ABLINER_API_KEY']}"},
timeout=15,
)
print(res.json())Security
Authentication
All requests authenticate with an API key. Bearer authentication is recommended, andX-API-Key is also accepted for SDKs that cannot easily set Bearer auth. Keys are shown once at creation - store them in your server secrets. Never embed keys in mobile apps or browser code.
whsec_) is only for verifyingx-webhook-signature andx-webhook-timestamp. It is not the API key you send in Authorization: Bearer ....Authorization: Bearer tsl_live_xxxxxxxxxxxx
Content-Type: application/jsonAlternative header:
X-API-Key: tsl_live_xxxxxxxxxxxx
Content-Type: application/jsonCompromised a key? Revoke it from /dashboard/api-keys and generate a new one - subsequent requests with the old key return 401 within seconds.
What to use where
- API key: the value generated in your dashboard, sent as the Bearer token in every REST request.
- Webhook signing secret: the separate secret shown on the API Keys page, used only to verify webhook signatures in your own server.
Endpoints
Wallet
/api/v1/balanceGet your live TZS wallet balance and remaining SMS credits.
{
"status": "success",
"data": {
"balance": 1250,
"currency": "TZS",
"balances": { "TZS": 1250, "USD": 0, "KES": 0, "UGX": 0 }
}
}/api/v1/transactions?limit=50Recent transaction history for deposits, disbursements and SMS top-ups. The current API returns the latest items directly, so you can render them immediately in your dashboard or admin panel.
{
"status": "success",
"data": [
{
"id": "tx_01HZ...",
"type": "deposit",
"method": "mobile",
"amount_cents": 500000,
"status": "completed",
"created_at": "2026-05-31T08:21:00Z",
"reference": "INV-001"
}
]
}Collect
Deposits
Deposit or Checkout - which one?
Deposit (/api/v1/deposits) charges a mobile money number, card or QR. For method: "mobile" the payer gets a USSD push on the phone - there is no page to open and payment_url is null.
Checkout (/api/v1/checkout) creates a hosted payment page. The payer opens checkout_url and pays from their Abliner balance using their account number + TES. No phone number is needed.
Every response now carries type (deposit or checkout) and flow (push or hosted_page) so your SDK never mixes them up.
Initiate a Mobile Money (USSD push), Card or Dynamic QR collection. Abliner uses the configured payment route. An accepted request is not proof that the prompt reached the phone or that payment completed; confirm payment through status lookup or callbacks.
/api/v1/depositsAlways send currency - the ISO code of the money you are collecting (for example TZS, UGX, USD). It must match the country of the phone number you charge. When omitted we default to TZS.
{
"amount": 1000,
"method": "mobile",
"currency": "TZS",
"phone": "255712345678",
"reference": "ORDER-2026-001",
"callback_url": "https://yourapp.com/webhooks/abliner"
}Response - for mobile a USSD push is triggered immediately; for card / dynamic-qr you redirect the customer to payment_url.
{
"status": "success",
"data": {
"id": "tx_01HZABCDEF...",
"type": "deposit",
"flow": "push",
"status": "pending",
"amount": 1000,
"currency": "TZS",
"method": "mobile",
"payment_url": null,
"reference": "pp3k8fa91c2b7d",
"customer_reference": "ORDER-2026-001",
"message": "Mobile money push sent. Ask the payer to approve the prompt on their phone."
}
}Minimum deposit is TZS 500 and maximum is TZS 3,000,000 per transaction. phone is required when method is mobile. Missing or invalid fields return 422 validation_failed naming the exact field - never a 502. Your reference may contain dashes; we return it as customer_reference and send an alphanumeric reference to the mobile network, which is what webhooks echo.
If the phone still has an active payment prompt, the API returns 409 ussd_session_busy. Ask the payer to approve or dismiss the existing prompt. If none is visible, wait for the mobile session to expire. Check the previous transaction before starting a new attempt with a new Idempotency-Key; never automatically send another push after a timeout. Do not use the phone number as your payment key.
Node.js example
const BASE_URL = process.env.ABLINER_API_BASE_URL ?? "https://abliner.net";
const res = await fetch(`${BASE_URL}/api/v1/deposits`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ABLINER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 1000,
method: "mobile",
currency: "TZS",
phone: "255712345678",
reference: "ORDER-2026-001",
callback_url: "https://yourapp.com/webhooks/abliner",
}),
});
const tx = await res.json();Final status is delivered via webhook (transaction.completed or transaction.failed) - do not block on the synchronous response.
Hosted payment
Abliner Checkout
Create a hosted payment session from your server using an Abliner API key. Send your customer to the returned checkout_url. The customer enters their Abliner account number and 4-digit TES, confirms their loaded name, and approves the payment.
After a successful payment, Abliner sends checkout.paid to yourcallback_url and redirects the customer to your redirect_url. Keep the API key on your server; never create Checkout sessions directly from browser code.
/api/v1/checkout{
"amount": 500,
"currency": "TZS",
"title": "Aston Shop",
"description": "Payment for order 1024",
"reference": "ORDER-1024",
"callback_url": "https://yourshop.com/webhooks/abliner",
"redirect_url": "https://yourshop.com/orders/1024/complete",
"expires_in_minutes": 120
}The minimum TZS amount is TZS 500. amount is required. Currency defaults to TZS; the remaining fields are optional, but providecallback_url for reliable server confirmation and redirect_urlto return the customer to your website.
Create response
{
"status": "success",
"data": {
"id": "3f70b8d4-...",
"token": "a1b2c3d4...",
"checkout_url": "https://abliner.net/checkout/a1b2c3d4...",
"amount": 500,
"currency": "TZS",
"reference": "ORDER-1024",
"status": "pending",
"expires_at": "2026-09-13T03:00:00.000Z"
}
}Check payment status
/api/v1/checkout?reference=ORDER-1024You may query with ?reference= or ?id=. Both calls require the same Bearer API key and only return sessions owned by that key's account.
Payment callback
{
"event": "checkout.paid",
"id": "3f70b8d4-...",
"token": "a1b2c3d4...",
"reference": "ORDER-1024",
"amount": 500,
"currency": "TZS",
"payer_account": "1000000001",
"payer_name": "CUSTOMER NAME",
"paid_at": "2026-09-13T01:05:00.000Z"
}Clear validation errors
If a required or invalid field is submitted, the API returns HTTP 422with the exact field and reason so you can correct the integration.
{
"status": "error",
"code": "validation_failed",
"message": "Invalid or missing field 'amount': amount is required and must be a number.",
"field": "amount"
}Disburse
Disbursements
Disburse money to a mobile wallet or bank account.
/api/v1/withdrawalsInclude currency (for example TZS, KES, UGX) so the payout is settled in the recipient's local money. Defaults to TZS.
Mobile wallet
{
"amount": 5000,
"method": "mobile",
"currency": "TZS",
"recipient": "255712345678",
"reference": "PAYOUT-001"
}Bank account
{
"amount": 50000,
"method": "bank",
"recipient": "0150123456789",
"bank_code": "CRDB",
"account_name": "John Doe",
"reference": "PAYOUT-002"
}Supported bank codes: CRDB, NMB, NBC, EQUITY, EXIM, STANBIC, DTB, AKIBA, AZANIA, BOA.
Fee preview
/api/v1/withdrawals/preview?method=mobile&amount=50000{
"status": "success",
"data": {
"amount_tzs": 50000,
"fee_tzs": 2500,
"total_debited_tzs": 52500
}
}Verify
Name Lookup
Check the registered name of a mobile money number before you disburse money, so your customers always send to the right person.
/api/v1/lookup?phone=255712345678curl "https://abliner.net/api/v1/lookup?phone=255712345678" \
-H "Authorization: Bearer YOUR_API_KEY"Response
{
"status": "success",
"data": {
"phone": "255712345678",
"name": "JOHN JUMA DOE",
"network": "vodacom"
}
}If the number cannot be verified you receive 404 with name_not_found - double-check the number before retrying. Accepts any common format (0712…, 712…, +255…).
BillPay
Control Numbers
Generate a one-time control number your customer can pay from any bank or mobile money app. Powered by ClickPesa BillPay - this feature stays available even when ClickPesa is not the active deposit gateway.
/api/v1/control-numbers{
"amount": 10000,
"description": "Invoice INV-001",
"payment_mode": "EXACT",
"reference": "INV-001",
"customer_name": "John Doe"
}payment_mode may be EXACT (customer must pay the exact amount) or PARTIAL. customer_name is optional - when provided we create a named customer control number instead of an order one.
{
"status": "success",
"data": {
"id": "tx_01HZ...",
"control_number": "993412345678",
"reference": "INV-001",
"amount": 10000,
"payment_mode": "EXACT",
"description": "Invoice INV-001"
}
}Once the customer pays, the same webhook you configured for deposits fires (transaction.completed) and the wallet is credited automatically.
How your customer pays
Show these steps to your customers. Choose Mobile Money or Bank to see each channel.
How will the customer pay?
Messaging
Bulk SMS
Send transactional or marketing SMS using your approved Sender ID when you have one, or fall back to the default ABLINER sender. Credits are pre-paid from your wallet.
/api/v1/sms/balance{
"status": "success",
"data": {
"sms_credits": 1500,
"rate_tzs": 28
}
}/api/v1/sms/sender-ids[
{ "sender_id": "MYSHOP", "status": "approved" },
{ "sender_id": "PROMO", "status": "pending" }
]/api/v1/sms/send{
"recipient": "255712345678",
"message": "Welcome MYSHOP! Order #1234 imepokelewa.",
"sender_id": "MYSHOP"
}/api/v1/sms/send-bulk{
"sender_id": "MYSHOP",
"message": "Promo siku 3 tu - 20% punguzo!",
"recipients": ["255712345678", "255756111222", "255744333444"]
}PHP example
<?php
$baseUrl = getenv("ABLINER_API_BASE_URL") ?: "https://abliner.net";
$ch = curl_init($baseUrl . "/api/v1/sms/send");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . getenv("ABLINER_API_KEY"),
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"recipient" => "255712345678",
"message" => "Hello from PHP!",
"sender_id" => "MYSHOP",
]));
$res = json_decode(curl_exec($ch), true);⚠️ You need SMS credits before sending. If you have an approved Sender ID, it will be used; otherwise the default ABLINER sender is used. Buy credits at /dashboard/sms.
Realtime
Webhooks
Required: set your webhook callback URL first.
Abliner only sends payment status updates to a URL you have given us. If you have not set one, no status is ever sent and transactions will look pending forever in your system — even when the customer has paid.
To fix this, do one of the following:
- Recommended: open your Abliner Dashboard > API Keys and save your HTTPS URL under Add Your URL callback. This URL is then used automatically for every transaction.
- Or pass
callback_urlin each API request (deposit, withdrawal, checkout). A per-requestcallback_urloverrides the saved one.
After saving, every completed or failed payment is POSTed to your URL about 25 seconds after it finishes, signed with your webhook secret.
To receive final transaction updates, pass your own HTTPS callback_url in your API request. Abliner will POST JSON to that URL whenever a transaction reaches a final state. Each request is signed so you can verify it came from Abliner.
Find your webhook signing secret on Dashboard > API Keys. Tap Generate personal secret to get one unique to your account. Use it in your server only and never expose it in browser code.
This is your endpoint in your own app or system. You do not copy an Abliner callback URL back into your own webhook receiver.
transaction.completed
{
"id": "evt_3f2a9c1e-7b4d-4e2a-9f10-2c6d8e1a5b77_completed",
"event": "transaction.completed",
"created_at": "2026-10-05T08:23:11.000Z",
"data": {
"id": "3f2a9c1e-7b4d-4e2a-9f10-2c6d8e1a5b77",
"type": "deposit",
"method": "mobile",
"amount": 5000,
"amount_cents": 500000,
"currency": "TZS",
"status": "completed",
"reference": "ABL8F2K1Q9",
"customer_reference": "UPATU-CH-1029",
"provider_reference": "PRV-77812345",
"recipient": null,
"completed_at": "2026-10-05T08:23:11.000Z"
}
}transaction.failed
{
"id": "evt_3f2a9c1e-7b4d-4e2a-9f10-2c6d8e1a5b77_failed",
"event": "transaction.failed",
"created_at": "2026-10-05T08:24:40.000Z",
"data": {
"id": "3f2a9c1e-7b4d-4e2a-9f10-2c6d8e1a5b77",
"type": "deposit",
"method": "mobile",
"amount": 5000,
"amount_cents": 500000,
"currency": "TZS",
"status": "failed",
"reference": "ABL8F2K1Q9",
"customer_reference": "UPATU-CH-1029",
"provider_reference": null,
"recipient": null,
"completed_at": "2026-10-05T08:24:40.000Z"
}
}amountis in whole units (TZS 5000).amount_centsis the same value × 100.referenceis the Abliner reference;customer_referenceis thereferenceyou sent;provider_referenceis the mobile network reference when available.id(also sent as headerx-webhook-id) is stable across retries. Store it and ignore events you already processed.
Possible events: transaction.completed, transaction.failed, withdrawal.completed, withdrawal.failed (disbursement events keep the withdrawal.* names for compatibility with existing integrations).
Signature verification (Node.js)
x-webhook-timestamp is Unix time in seconds. The signature in x-webhook-signature is the hex HMAC-SHA256 of${timestamp}.${rawBody} using your webhook signing secret. Every retry is signed again with a fresh timestamp. Reject timestamps older than 5 minutes.
import crypto from "crypto";
// Store this as an environment variable: ABLINER_WEBHOOK_SECRET=whsec_...
const secret = process.env.ABLINER_WEBHOOK_SECRET;
export function verify(req) {
const signature = req.headers["x-webhook-signature"];
const timestamp = req.headers["x-webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const rawBody = req.rawBody.toString();
const message = `${timestamp}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(message).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature ?? "", "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Respond with any 2xx within 8 seconds. If not, we retry up to 2 more times (after about 2s and 5s). If all attempts fail, reconcile with GET /api/v1/transactions.
Avoiding double charges (Idempotency-Key)
Required for new API keys. Send a unique Idempotency-Key header on POST /api/v1/deposits (for example your own payment ID). API keys created from 5 Oct 2026 onward get 400 idempotency_key_required without it, and 400 webhook_secret_required (no push sent) until a webhook signing secret is generated in Dashboard → API Keys. If you retry with the same key within 24 hours:
- Same amount and currency: you get the original transaction back (
200,"idempotent_replay": true). No new USSD push is sent. - Different amount or currency:
409 idempotency_conflict. - First request still running:
409 request_in_progress. Retry after a few seconds. - A failed original attempt returns an error, not a successful push replay. A new payment attempt needs a new key; the same key never sends another push.
- Without the header, every request creates a new payment, even when the
referencerepeats.
curl -X POST https://abliner.net/api/v1/deposits \
-H "Authorization: Bearer tsl_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: UPATU-PAY-1029" \
-d '{"amount":5000,"method":"mobile","phone":"255712345678","currency":"TZS","reference":"UPATU-CH-1029","callback_url":"https://yourapp.com/webhooks/abliner"}'Lookup after a timeout
If your request timed out before you got a transaction ID, look it up by your key or your reference:
GET /api/v1/transactions?idempotency_key=UPATU-PAY-1029
GET /api/v1/transactions?customer_reference=UPATU-CH-1029
GET /api/v1/transactions?id=3f2a9c1e-7b4d-4e2a-9f10-2c6d8e1a5b77{
"status": "success",
"data": [
{
"id": "3f2a9c1e-7b4d-4e2a-9f10-2c6d8e1a5b77",
"type": "deposit",
"method": "mobile",
"amount": 5000,
"amount_cents": 500000,
"currency": "TZS",
"status": "pending",
"reference": "ABL8F2K1Q9",
"customer_reference": "UPATU-CH-1029",
"provider_reference": null,
"created_at": "2026-10-05T08:22:58.000Z"
}
]
}An empty data array means no transaction was created and no push was sent, so it is safe to retry. Safest of all: retry with the same Idempotency-Key, which never charges twice.
Testing
There is no sandbox. Test with small live amounts (minimum TZS 500). You need an API key, and business verification when your account requires it.
Throughput
Rate Limits
API requests are rate-limited per API key:
- 120 req/min on read endpoints (
/balance,/transactions). - 60 req/min on payment endpoints (
/deposits,/withdrawals). - 30 req/sec on
/sms/sendand 5 req/sec on/sms/send-bulk(max 1,000 recipients per call).
Daily collection limit
Each developer account can collect up to TZS 150,000,000 per day through the API (/deposits and /checkout). The limit resets at 00:00 East Africa Time. Requests above the remaining amount return 422 with code daily_limit_exceeded. Abliner stores and marketplace sales have no daily limit.
When you exceed a limit we return 429 Too Many Requests with these headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 12Need higher limits? Reach support inside the dashboard - we'll lift them for Business-verified accounts.
Reliability
Errors
Every /api/v1/* response is always JSON - including authentication failures, validation failures and unexpected server errors. You can safely call response.json() on any status code. Error responses share this shape:
{
"status": "error",
"code": "insufficient_funds",
"message": "Insufficient balance.",
"retryable": false,
"field": "amount", // only for validation errors
"transaction_id": "7683..." // only when a transaction was created
}retryable: true means the same request can succeed later (upstream rail hiccup). retryable: false means the request must be changed before retrying.
400invalid_json- body was empty or not a JSON object. SendContent-Type: application/json.401unauthorized- missing, invalid or revoked API key402insufficient_funds- wallet balance too low403- account suspended, or API access locked until business verification is approved404not_found- unknown id or reference422validation_failed- a field is missing or invalid;fieldanderrorstell you exactly which one422amount_out_of_range- below the minimum (TZS 500 for deposits and Checkout) or above the maximum (TZS 3,000,000 per deposit)422unsupported_request- the network, country or payment method is not enabled right now (for example a blocked mobile network)422invalid_phone- the phone number is not a valid mobile money number for that country502provider_error- the payment rail rejected or did not answer. Retry with backoff.503gateway_not_configured- that rail is temporarily switched off on our side500internal_error- unexpected error on our side; retry with backoff
Handling responses safely
Check the HTTP status and read code - do not branch on the message text, which may be reworded.
const res = await fetch(`${BASE_URL}/api/v1/deposits`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ABLINER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const raw = await res.text(); // read raw text first
let body;
try {
body = JSON.parse(raw);
} catch {
throw new Error(`Unexpected non-JSON response (${res.status}): ${raw.slice(0, 200)}`);
}
if (!res.ok) {
// body.code: validation_failed | insufficient_funds | provider_error | ...
if (body.retryable) {
// safe to retry the same request after a short delay
}
throw new Error(body.message);
}
const deposit = body.data;Include the transaction_id (or your reference) when contacting support - it lets us locate the exact request in seconds.
Ready to go live?
Generate a production API key and start integrating.
