Getting started

View Source

This guide takes you from nothing to a working M-Pesa payment in an Elixir application.

Which M-Pesa?

elixir_mpesa targets the Vodacom/Vodafone M-Pesa OpenAPI at openapi.m-pesa.com, which serves Tanzania, Lesotho, Ghana and the DRC.

If you are integrating M-Pesa in Kenya, you need Safaricom's Daraja API instead — a completely different service. This library will not work for it.

1. Get credentials

Register an application at the M-Pesa OpenAPI Portal. You will be given:

  • an API key — a secret, unique to your application
  • a public key — the RSA key your API key is encrypted with, per market
  • a service provider code — your business shortcode

The portal issues separate credentials for sandbox and production.

2. Install

def deps do
  [{:elixir_mpesa, "~> 0.2.0"}]
end

Requires Elixir 1.15 or later.

3. Configure

Credentials are secrets. Put them in config/runtime.exs, read from the environment — never in config/config.exs, which is committed.

# config/runtime.exs
import Config

config :elixir_mpesa,
  api_type: "sandbox",
  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")

Setting market: :tanzania fills in the URL context, country code and currency together, so they cannot drift apart. See Markets for the full table, and Authentication for more on credentials.

Switch to production by changing one value:

api_type: "openapi"

4. Take a payment

attrs = %{
  "input_Amount" => "10",
  "input_CustomerMSISDN" => "255700000000",
  "input_TransactionReference" => "INV-1024",
  "input_ThirdPartyConversationID" => ElixirMpesa.conversation_id(),
  "input_PurchasedItemsDesc" => "Order 1024"
}

case ElixirMpesa.c2b(attrs) do
  {:ok, response} ->
    Logger.info("paid: #{response.transaction_id}")
    {:ok, response.transaction_id}

  {:error, %ElixirMpesa.Error{} = error} ->
    Logger.error("payment failed: #{Exception.message(error)}")
    {:error, error.reason}
end

That is the whole flow. Note what you did not have to do:

  • No session key. ElixirMpesa.Session obtains one on first use, caches it per market, refreshes it before it expires and re-authenticates once if M-Pesa rejects it.
  • No "input_Country", "input_Currency" or "input_ServiceProviderCode". They come from configuration, and you only pass them when overriding.

5. Handle the outcome

Every function returns {:ok, ElixirMpesa.Response.t()} or {:error, ElixirMpesa.Error.t()}. Match on error.reason, which is a documented atom:

case ElixirMpesa.c2b(attrs) do
  {:ok, response} -> ...
  {:error, %ElixirMpesa.Error{category: :config} = error} -> raise error   # your bug
  {:error, %ElixirMpesa.Error{category: :transport}} -> :retry_later       # network
  {:error, %ElixirMpesa.Error{category: :api, code: code}} -> handle(code) # M-Pesa said no
end

See Error codes for the full breakdown.

If you would rather have failures raise, every function has a ! variant:

response = ElixirMpesa.c2b!(attrs)

Retrying safely

"input_ThirdPartyConversationID" is the idempotency key. Generate one per logical transaction with ElixirMpesa.conversation_id/0, and if you retry that transaction, send the same one again. M-Pesa recognises the duplicate and rejects it instead of charging the customer twice.

This is why the library never retries a payment for you, and why it refuses to generate a conversation ID for one. See Payments.

Next steps

  • Markets — supported countries and how to add another
  • Authentication — credentials and the session lifecycle
  • Payments — C2B, B2C, B2B and reversals
  • Direct debit — mandates and recurring collection
  • Testing — testing your integration with no network