AirtelMoney

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

Features

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

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

The SDK supports both authenticated and unauthenticated webhooks as per the official Airtel Money API documentation.

Webhook Payload Format

Airtel sends transaction status updates to your callback URL with the following format:

{
"transaction": {
"id": "BBZMiscxy",
"message": "Paid KES 5,000 to TECHNOLOGIES LIMITED",
"status_code": "TS",
"airtel_money_id": "MP210603.1234.L06941"
},
"hash": "zITVAAGYSlzl1WkUQJn81kbpT5drH3koffT8jCkcJJA="
}

Status codes:

Using the Plug (Phoenix)

With Authentication (Recommended for Production):

pipeline :webhooks do
plug AirtelMoney.WebhookPlug
end
scope "/webhooks" do
pipe_through :webhooks
post "/airtel", WebhookController, :handle
end

Without Authentication (For Testing):

pipeline :webhooks do
plug AirtelMoney.WebhookPlug, require_auth: false
end
scope "/webhooks" do
pipe_through :webhooks
post "/airtel", WebhookController, :handle
end

The plug will:

Webhook Controller Example

defmodule MyAppWeb.WebhookController do
use MyAppWeb, :controller
def handle(conn, _params) do
# The plug already verified the signature and parsed the payload
webhook_data = conn.assigns[:airtel_webhook]
case webhook_data do
%{transaction: %{id: txn_id, status_code: status_code}} ->
# Update your database based on transaction status
if status_code == "TS" do
# Transaction successful - update order, send confirmation
end
if status_code == "TF" do
# Transaction failed - notify customer, handle retry
end
_ ->
IO.puts("Unknown webhook format")
end
send_resp(conn, 200, "OK")
end
end

Manual Webhook Verification

# Verify webhook signature (hash is extracted from JSON body automatically)
case AirtelMoney.verify_webhook(payload) do
:ok ->
# Signature is valid, process webhook
{:ok, webhook_data} = AirtelMoney.parse_webhook(payload)
# Handle webhook_data
{:error, :invalid_signature} ->
# Invalid signature
{:error, :missing_hash} ->
# Hash not found in payload
{:error, :webhook_secret_not_configured} ->
# Webhook secret not configured
end

Telemetry

The SDK emits telemetry events for monitoring:

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.