AzamPay

Made in Tanzania

A pure-Dart SDK for the AzamPay payment APIs, covering Tanzania, Rwanda and International Money Transfer (IMT). It works in Flutter, server-side Dart and CLI apps.

Official AzamPay developer docs: developerdocs.azampay.co.tz/

Highlights

  • Full API coverage — collections, checkout, OTP, disbursements, transfers, balance, name lookup and transaction status across all three regions.
  • Automatic auth — tokens are generated and cached per region; you never call the token endpoint yourself.
  • Self-documenting — every endpoint has a .doc you can print while coding (path, request/response fields and a runnable example).
  • Typed & safe — enums for providers, a single AzamPayResponse wrapper, and AzamPayException for transport errors.
  • Testable — inject your own http.Client (e.g. a MockClient).

Install

dependencies:
  azampay: ^1.0.0
import 'package:azampay/azampay.dart';

Quick start

final azampay = AzamPay(
  appName: '<your app name>',
  clientId: '<your client id>',
  clientSecret: '<your client secret>',
  // sandbox is the default; pass sandbox: false for production.
  // sandbox: false,
);

final res = await azampay.tanzania.mnoCheckout(
  accountNumber: '<Customer Phone Number>', // country-code prefixed (255...)
  amount: 1000,
  currency: 'TZS',
  provider: MnoProvider.azampesa, // Airtel | Tigo | Halopesa | Azampesa | Mpesa
  externalId: '<your unique reference>',
);

if (res.success == true) {
  print('Reference: ${res.transactionId}');
} else {
  print('Failed: ${res.message}');
}

Everything is reached through a region: azampay.tanzania, azampay.rwanda, azampay.imt.

Phone numbers must include the country code — 255... for Tanzania, 250... for Rwanda.

externalId / referenceId must be unique per transaction (AzamPay uses it to de-duplicate). A good pattern is a prefix plus a timestamp or UUID:

String reference(String prefix) =>
    '$prefix-${DateTime.now().millisecondsSinceEpoch}';

The response object

Every call returns an AzamPayResponse:

Getter Meaning
isSuccessful HTTP status is 2xx
statusCode HTTP status code
success AzamPay's success flag
message AzamPay's message
data AzamPay's data payload
transactionId Reference (any of transactionId / pgReferenceId / referenceId)
body The decoded JSON (Map/List)
rawBody The verbatim response string
field<T>('key') Any top-level field

A normal business failure (e.g. insufficient balance) is returned as a response — inspect isSuccessful / success. AzamPayException is only thrown for transport failures (network down, bad credentials, unparseable token).

Self-documenting endpoints

Every endpoint is a callable object that also carries its documentation:

// Call it:
await azampay.tanzania.mnoCheckout(...);

// Read its full docs inline (path, fields, example):
print(azampay.tanzania.mnoCheckout.doc);

// List everything a region offers:
print(azampay.rwanda.describe());

print(azampay.tanzania.mnoCheckout.doc) prints:

┌─ mnoCheckout
│  POST /azampay/mno/checkout
│  Mobile-money (MNO) push checkout.
│
│  Request body:
│  accountNumber         string   required  Customer mobile number to charge
│  amount                number   required
│  currency              string   required  e.g. TZS
│  provider              string   required  Mobile operator [Airtel, Tigo, Halopesa, Azampesa, Mpesa]
│  externalId            string   required  Your unique transaction reference
│  ...
│  Example:
│    await azampay.tanzania.mnoCheckout(...);
└─

Tanzania

final tz = azampay.tanzania;
Endpoint Description
mnoCheckout(...) Mobile-money push checkout
bankCheckout(...) Bank checkout (confirmed with an OTP)
generateCrdbOtp({...}) / generateNmbOtp({...}) Request a bank OTP
getPaymentPartners() List hosted-checkout partners
postCheckout(...) Create a hosted checkout page
disburse(...) Pay out to a wallet
nameLookup(...) Resolve a disbursement account name
transactionStatus(...) Disbursement transaction status

Mobile checkout — charge a customer's wallet.

await tz.mnoCheckout(
  accountNumber: '<Customer Phone Number>', // 255...
  amount: 1000,
  currency: 'TZS',
  provider: MnoProvider.azampesa, // Airtel | Tigo | Halopesa | Azampesa | Mpesa
  externalId: '<your unique reference>',
);

Bank checkout — charge a bank account (generate an OTP first).

await tz.bankCheckout(
  merchantAccountNumber: '<Your Merchant Account Number>',
  merchantMobileNumber: '<Customer Phone Number>',
  amount: 5000,
  currencyCode: 'TZS',
  provider: BankProvider.crdb, // CRDB | NMB
  otp: '<OTP from generateCrdbOtp / generateNmbOtp>',
  referenceId: '<your unique reference>',
);

Disbursement — pay money out, from your wallet to a recipient.

await tz.disburse(
  // SOURCE = you (the sender / payer).
  source: const DisbursementAccount(
    accountNumber: '<Your Payout Wallet Number>',
    fullName: '<Your Company Name>',
    bankName: 'Azampesa', // Airtel | Tigo | Azampesa
    currency: 'TZS',
    countryCode: 'TZ',
  ),
  // DESTINATION = the recipient.
  destination: const DisbursementAccount(
    accountNumber: '<Recipient Phone Number>',
    fullName: '<Recipient Name>',
    bankName: 'Tigo', // Airtel | Tigo | Azampesa
    currency: 'TZS',
    countryCode: 'TZ',
  ),
  transferDetails: const TransferDetails(amount: 2000, type: 'SendMoney'),
  externalReferenceId: '<your unique reference>',
);

Rwanda

final rw = azampay.rwanda;

Collection (v1): checkout, transactionStatus, transactionStatusByReference, accountLookup. Disbursement (v1): nameLookup, checkBalance, disburse, disbursementStatus. v2: accountLookupV2, initiatePayment, paymentStatus, paymentStatusByReference, balance, initiateTransfer, transferStatus, transferStatusByReference.

final payment = await rw.initiatePayment(
  provider: 'Airtel',
  currencyCode: 'RWF',
  amount: '1000',
  referenceId: '<your unique reference>',
  accountNumber: '<Customer Phone Number>', // 250...
);

final status = await rw.paymentStatus(pgReferenceId: payment.transactionId!);

International Money Transfer (IMT)

final imt = azampay.imt; // sendMoney, nameLookup, checkBalance, transactionStatus

IMT payloads are compliance-heavy (sender identity, nationality, reason…), so sendMoney takes the source / destination / transferDetails objects as maps. Print azampay.imt.sendMoney.doc for the exact fields.

await imt.sendMoney(
  source: {
    'fullName': '<Sender Name>',
    'nationality': '<Sender Country Code>',
    // ... see azampay.imt.sendMoney.doc
  },
  destination: {
    'fullName': '<Recipient Name>',
    'bankName': 'Azampesa',
    'accountNumber': '<Recipient Account Number>',
  },
  transferDetails: {'amount': 50000, 'dateInEpoch': 1700000000},
  externalReferenceId: '<your unique reference>',
  checksum: '<checksum>',
  remarks: '<remarks>',
);

Environments & custom hosts

Sandbox is the default. Switch to production with sandbox: false (or environment: AzamEnvironment.production).

AzamPay splits its API across several hostnames (auth / checkout / disbursement, different per country). Sandbox hosts come straight from the official specs; a few production hosts aren't published there and are derived by convention. You can override any host:

final azampay = AzamPay(
  appName: '<your app name>',
  clientId: '<your client id>',
  clientSecret: '<your client secret>',
  sandbox: false,
  baseUrlOverrides: {
    // key: "<region>.<service>.<environment>"
    'rwanda.checkout.production': 'https://checkout.azampay.co.rw',
  },
);

Testing

Inject a mock client — no network needed:

import 'package:http/testing.dart';
import 'package:http/http.dart' as http;

final azampay = AzamPay(
  appName: '<your app name>',
  clientId: '<your client id>',
  clientSecret: '<your client secret>',
  httpClient: MockClient((request) async {
    if (request.url.path.contains('GenerateToken')) {
      return http.Response('{"data":{"accessToken":"t"},"success":true}', 200);
    }
    return http.Response('{"success":true,"transactionId":"txn-1"}', 200);
  }),
);

Run the SDK's own suite with dart test.

Migrating from 0.0.x

The old top-level calls still work (deprecated):

Old New
azampay.accessToken azampay.token()
azampay.mobileCheckout(merchantMobileNumber: ...) azampay.tanzania.mnoCheckout(accountNumber: ...)
azampay.bankCheckout(currency: ...) azampay.tanzania.bankCheckout(currencyCode: ...)

The main change: calls now return an AzamPayResponse (with success, message, data, transactionId) instead of a raw http.Response.

Credits

Built and maintained by Brightius Kalokola at TRIXA. Thanks to AzamPay for the payment platform.

Support

Please open an issue on GitHub, or contact the maintainer at brightius@trixa.net

Licensed under the MIT License.

Libraries

azampay
AzamPay Dart SDK.