ElixirMpesa
View SourceM-Pesa mobile money payments for Elixir. A client for the Vodacom/Vodafone M-Pesa OpenAPI, covering customer-to-business (C2B), business-to-customer (B2C), business-to-business (B2B), reversals, direct debit mandates and transaction queries across Tanzania, Lesotho, Ghana and the DR Congo.
[!IMPORTANT] This is the Vodacom M-Pesa OpenAPI (
openapi.m-pesa.com). If you are integrating M-Pesa in Kenya, you need Safaricom's Daraja API and a different library — the endpoints, authentication and payloads are unrelated.
{:ok, response} = ElixirMpesa.c2b(%{
"input_Amount" => "10",
"input_CustomerMSISDN" => "255700000000",
"input_TransactionReference" => "INV-1024",
"input_ThirdPartyConversationID" => ElixirMpesa.conversation_id(),
"input_PurchasedItemsDesc" => "Order 1024"
})
response.transaction_id
#=> "49XCD123F6"No session handshake, no country and currency on every call, no string-keyed error maps.
Installation
def deps do
[{:elixir_mpesa, "~> 0.2.0"}]
endRequires Elixir 1.15+ and OTP 25+.
Configuration
Credentials are secrets — put them in config/runtime.exs, read from the environment.
import Config
config :elixir_mpesa,
api_type: "sandbox", # "openapi" for production
market: :tanzania,
service_provider_code: System.get_env("MPESA_SERVICE_PROVIDER_CODE"),
api_key: System.fetch_env!("MPESA_API_KEY"),
public_key: System.fetch_env!("MPESA_PUBLIC_KEY")Get credentials from the M-Pesa OpenAPI Portal.
Supported markets
Setting :market fills in the URL context, country code and currency together, so they
cannot drift apart.
:market | Country | URL context | Country code | Currency |
|---|---|---|---|---|
:tanzania | Tanzania | vodacomTZN | TZN | TZS |
:lesotho | Lesotho | vodacomLES | LES | LSL |
:ghana | Ghana | vodafoneGHA | GHA | GHS |
:drc | DR Congo | vodacomDRC | DRC | CDF |
A market without a preset works too — set url_context, country and currency
directly. Sessions are cached per market, so one application can serve several countries
at once. See the Markets guide.
Operations
| Function | Operation |
|---|---|
c2b/2 | Customer pays your business |
b2c/2 | Your business pays a customer — refunds, payouts, salaries |
b2b/2 | Your business pays another business |
reversal/2 | Reverse a completed transaction |
query_transaction_status/2 | Look up a transaction |
query_beneficiary_name/2 | Look up the name behind a phone number |
direct_debit_creation/2 | Create a mandate |
direct_debit_payment/2 | Collect against a mandate |
query_direct_debit/2 | Check a mandate |
direct_debit_cancel/2 | Cancel a mandate |
Each has a ! variant that returns the response directly and raises on failure.
Sessions are handled for you
The OpenAPI requires a session key obtained by encrypting your API key, exchanging it at
getSession, then encrypting the result. ElixirMpesa.Session does this on first use and
then caches the key per market, refreshes it before its one-hour expiry, collapses
concurrent cache misses into a single getSession call, and re-authenticates once if
M-Pesa rejects it mid-flight.
You can still drive it manually — see Authentication.
Error handling
Every function returns {:ok, ElixirMpesa.Response.t()} or
{:error, ElixirMpesa.Error.t()}. Match on reason and category:
case ElixirMpesa.c2b(attrs) do
{:ok, response} ->
confirm(response.transaction_id)
# M-Pesa gave a definite answer: the money did not move.
{:error, %ElixirMpesa.Error{category: :api, code: code}} ->
decline(code)
# Timeout or gateway failure — the outcome is unknown. Query, do not resend.
{:error, %ElixirMpesa.Error{category: category}} when category in [:transport, :http] ->
reconcile_later()
endSee Error codes.
Retrying safely
"input_ThirdPartyConversationID" is the idempotency key. Generate one per transaction
with ElixirMpesa.conversation_id/0 and reuse the same one when retrying that
transaction — M-Pesa uses it to reject the duplicate rather than charge twice.
This is why the library never retries a payment automatically, and refuses to generate a conversation ID for one. Read-only queries get one generated.
Testing
Built on Req, so your suite runs against a stub with no network:
config :elixir_mpesa, req_options: [plug: {Req.Test, ElixirMpesa.Client}]Req.Test.stub(ElixirMpesa.Client, fn conn ->
Req.Test.json(conn, %{"output_ResponseCode" => "INS-0", "output_TransactionID" => "TX1"})
end)See the Testing guide.
Documentation
Using an AI coding assistant? llms.txt is a
condensed, machine-readable summary of the whole API.
Upgrading from 0.1.0
0.1.0 code keeps working — the old functions remain as deprecated shims until 0.3.0. You should upgrade regardless: 0.1.0 disabled TLS certificate verification on every request, and crashed rather than returning an error tuple on several ordinary HTTP statuses. See Upgrading.
Contributing
Issues and pull requests welcome at github.com/jamesnjovu/elixir_mpesa.
One contribution would be especially valuable: the INS-* response code table.
Vodacom publishes it only inside the authenticated developer portal, so this library
deliberately does not guess at code meanings. If you have that documentation, adding it to
ElixirMpesa.Error would help everyone.
mix deps.get
mix test # or: mix ci — format, credo, dialyzer and tests
License
MIT — see LICENSE.