An Elixir SDK for Airtel Money APIs, providing a clean and idiomatic interface for collections, disbursements, transaction queries, and webhooks.

Features

  • Collections - Receive payments from customers (USSD Push)
  • Disbursements - Send payments to customers with PIN encryption
  • Transfer Status - Check disbursement transfer status
  • Transaction Status - Query collection transaction status
  • Balance Queries - Check account balance
  • OAuth Token Management - Automatic token handling and refresh
  • PIN Encryption - RSA encryption for disbursement PINs
  • MSISDN Validation - Phone number format validation
  • Webhook Verification - HMAC SHA256 signature verification
  • Telemetry - Built-in telemetry events for monitoring
  • OTP Supervision - Robust supervision tree for production use
  • Sandbox & Production - Support for both environments

Installation

Add airtel_money to your list of dependencies in mix.exs:

def deps do
  [
    {:airtel_money, "~> 0.1.0"}
  ]
end

Run:

mix deps.get

Configuration

Configure the SDK in your config/config.exs:

config :airtel_money,
  client_id: "your_client_id",
  client_secret: "your_client_secret",
  country: "CD",
  currency: "CDF",
  environment: :sandbox,
  webhook_secret: "your_webhook_secret" # Optional, for webhook verification

Configuration Options

  • :client_id (required) - Your Airtel Money client ID
  • :client_secret (required) - Your Airtel Money client secret
  • :country (required) - Country code (e.g., "CD" for Democratic Republic of Congo)
  • :currency (required) - Currency code (e.g., "CDF" for Congolese Franc)
  • :environment (optional) - :sandbox or :production (default: :sandbox)
  • :host (optional) - Custom API host (overrides default)
  • :timeout (optional) - HTTP request timeout in milliseconds (default: 15000)
  • :pool_size (optional) - Connection pool size (default: 10)
  • :webhook_secret (optional) - Webhook signature secret for verification
  • :rsa_public_key (optional) - RSA public key for PIN encryption (required for disbursements in production)

Usage

Start the Application

The SDK uses OTP supervision. Ensure the application is started:

# In your application.ex
children = [
  AirtelMoney.Application
]

Collect a Payment

case AirtelMoney.collect(%{
  amount: "1000",
  msisdn: "2439xxxxxxx",
  reference: "INV-001"
}) do
  {:ok, result} ->
    IO.inspect(result)

  {:error, %AirtelMoney.Error{message: message}} ->
    IO.puts("Error: #{message}")
end

Disburse a Payment

# For production, you need to encrypt the PIN first
case AirtelMoney.Encryption.encrypt_pin("1234") do
  {:ok, encrypted_pin} ->
    case AirtelMoney.disburse(%{
      amount: "5000",
      msisdn: "2439xxxxxxx",
      reference: "PAY-001",
      pin: encrypted_pin
    }) do
      {:ok, result} ->
        IO.inspect(result)

      {:error, %AirtelMoney.Error{message: message}} ->
        IO.puts("Error: #{message}")
    end

  {:error, reason} ->
    IO.puts("PIN encryption failed: #{reason}")
end

Query Transaction Status

case AirtelMoney.transaction_status("TXN123") do
  {:ok, status} ->
    IO.inspect(status)

  {:error, %AirtelMoney.Error{message: message}} ->
    IO.puts("Error: #{message}")
end

Query Balance

case AirtelMoney.balance() do
  {:ok, balance} ->
    IO.inspect(balance)

  {:error, %AirtelMoney.Error{message: message}} ->
    IO.puts("Error: #{message}")
end

Validate MSISDN

case AirtelMoney.Utils.validate_msisdn("243900000000") do
  :ok ->
    IO.puts("Valid MSISDN")

  {:error, reason} ->
    IO.puts("Invalid MSISDN: #{reason}")
end

Fetch RSA Public Key

case AirtelMoney.Encryption.fetch_public_key() do
  {:ok, public_key} ->
    IO.puts("Public key fetched successfully")
    # Store this key in your config for future use

  {:error, error} ->
    IO.puts("Failed to fetch public key: #{error.message}")
end

Webhooks

Verify Webhook Signature

# In your webhook controller
def handle(conn, params) do
  signature = get_req_header(conn, "x-airtel-signature")
  payload = conn.assigns[:raw_body]

  case AirtelMoney.verify_webhook(payload, signature) do
    :ok ->
      # Signature is valid, process webhook
      {:ok, webhook_data} = AirtelMoney.parse_webhook(payload)
      # Handle webhook_data
      send_resp(conn, 200, "OK")

    {:error, :invalid_signature} ->
      send_resp(conn, 401, "Invalid signature")
  end
end

Using the Plug (Phoenix)

Add the plug to your router:

pipeline :webhooks do
  plug AirtelMoney.WebhookPlug
end

scope "/webhooks" do
  pipe_through :webhooks
  post "/airtel", WebhookController, :handle
end

The plug will:

  • Verify the webhook signature
  • Parse the JSON payload
  • Assign the parsed data to conn.assigns[:airtel_webhook]
  • Return 401 if verification fails

Telemetry

The SDK emits telemetry events for monitoring:

  • [:airtel_money, :success] - Successful API request
  • [:airtel_money, :failure] - Failed API request

Attach handlers to monitor events:

:telemetry.attach(
  "airtel-money-handler",
  [:airtel_money, :success],
  &handle_event/4,
  nil
)

def handle_event([:airtel_money, event], measurements, metadata, _config) do
  IO.puts("Event: #{event}, Duration: #{measurements.duration}ms")
end

Error Handling

All API functions return {:ok, result} or {:error, %AirtelMoney.Error{}}.

%AirtelMoney.Error{
  code: "ERR001",
  message: "Invalid request",
  status: 400
}

Testing

Run tests:

mix test

Run tests with coverage:

mix test.ci

Development

Linting

mix lint

Setup

mix setup

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License.

Documentation

Full documentation is available at HexDocs.