ElixirMpesa (ElixirMpesa v0.2.0)

View Source

Vodacom M-Pesa OpenAPI client for Elixir.

Payments, reversals, direct debit and transaction queries across Tanzania, Lesotho, Ghana and the DRC.

This is the Vodacom OpenAPI, not Safaricom Daraja

If you are integrating M-Pesa in Kenya, you want the Daraja API and a different library. This one targets openapi.m-pesa.com, the pan-African Vodacom/Vodafone platform, whose endpoints, authentication and payload shapes are unrelated to Daraja's.

Setup

# config/runtime.exs
config :elixir_mpesa,
  api_type: "sandbox",
  market: :tanzania,
  service_provider_code: System.get_env("MPESA_SERVICE_PROVIDER_CODE"),
  api_key: System.get_env("MPESA_API_KEY"),
  public_key: System.get_env("MPESA_PUBLIC_KEY")

Setting :market fills in the country, currency and URL context together. See Markets and Authentication.

Taking 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} -> {:ok, response.transaction_id}
  {:error, %ElixirMpesa.Error{} = error} -> {:error, Exception.message(error)}
end

Country, currency and service provider code are filled in from configuration when you omit them, so most calls carry only what actually varies per transaction.

The session key is handled for you — 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. See Getting started.

Return values

Every function returns {:ok, ElixirMpesa.Response.t()} or {:error, ElixirMpesa.Error.t()}. Match on error.reason, which is a documented atom. Each function also has a ! variant that returns the response directly and raises on failure.

Idempotency

"input_ThirdPartyConversationID" is the idempotency key, and it is required for every payment. Use a fresh one per logical transaction — conversation_id/0 generates one — and reuse the same one when retrying, so M-Pesa can reject the duplicate rather than charging twice. Read-only queries get one generated automatically.

Summary

Payments

Pays another business.

Same as b2b/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Pays a customer from your business — disbursements, refunds, salaries.

Same as b2c/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Charges a customer and credits your business.

Same as c2b/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Reverses a completed transaction.

Same as reversal/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Queries

Looks up the registered name behind a phone number.

Same as query_beneficiary_name/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Looks up the status of a direct debit mandate.

Same as query_direct_debit/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Looks up the status of a transaction.

Same as query_transaction_status/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Direct debit

Cancels a direct debit mandate.

Same as direct_debit_cancel/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Creates a direct debit mandate against a customer.

Same as direct_debit_creation/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Collects a payment against an existing mandate.

Same as direct_debit_payment/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Session

Generates a unique conversation ID suitable for "input_ThirdPartyConversationID".

Discards the cached session so the next call re-authenticates.

Returns a valid encrypted session key, obtaining and caching one if needed.

Types

Request attributes, using M-Pesa's own input_* key names.

A unique identifier for one logical transaction. See conversation_id/0.

A market with a built-in preset. See ElixirMpesa.Config.markets/0.

A customer phone number in international format without a +, e.g. "255700000000".

Payments

b2b(attrs, opts \\ [])

@spec b2b(
  attrs(),
  keyword()
) :: result()

Pays another business.

Required attributes

  • "input_Amount"
  • "input_PrimaryPartyCode"
  • "input_ReceiverPartyCode"
  • "input_TransactionReference"
  • "input_PurchasedItemsDesc"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.b2b(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.b2b(attrs, market: :ghana)

b2b!(attrs, opts \\ [])

@spec b2b!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as b2b/2 but returns the response directly and raises ElixirMpesa.Error on failure.

b2c(attrs, opts \\ [])

@spec b2c(
  attrs(),
  keyword()
) :: result()

Pays a customer from your business — disbursements, refunds, salaries.

Required attributes

  • "input_Amount"
  • "input_CustomerMSISDN"
  • "input_TransactionReference"
  • "input_PaymentItemsDesc"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.b2c(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.b2c(attrs, market: :ghana)

b2c!(attrs, opts \\ [])

@spec b2c!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as b2c/2 but returns the response directly and raises ElixirMpesa.Error on failure.

c2b(attrs, opts \\ [])

@spec c2b(
  attrs(),
  keyword()
) :: result()

Charges a customer and credits your business.

Required attributes

  • "input_Amount"
  • "input_CustomerMSISDN"
  • "input_TransactionReference"
  • "input_PurchasedItemsDesc"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.c2b(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.c2b(attrs, market: :ghana)

c2b!(attrs, opts \\ [])

@spec c2b!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as c2b/2 but returns the response directly and raises ElixirMpesa.Error on failure.

reversal(attrs, opts \\ [])

@spec reversal(
  attrs(),
  keyword()
) :: result()

Reverses a completed transaction.

Required attributes

  • "input_TransactionID"
  • "input_ReversalAmount"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.reversal(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.reversal(attrs, market: :ghana)

reversal!(attrs, opts \\ [])

@spec reversal!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as reversal/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Queries

query_beneficiary_name(attrs, opts \\ [])

@spec query_beneficiary_name(
  attrs(),
  keyword()
) :: result()

Looks up the registered name behind a phone number.

Required attributes

  • "input_CustomerMSISDN"
  • "input_ThirdPartyConversationID" - optional; generated when omitted.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.query_beneficiary_name(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.query_beneficiary_name(attrs, market: :ghana)

query_beneficiary_name!(attrs, opts \\ [])

@spec query_beneficiary_name!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as query_beneficiary_name/2 but returns the response directly and raises ElixirMpesa.Error on failure.

query_direct_debit(attrs, opts \\ [])

@spec query_direct_debit(
  attrs(),
  keyword()
) :: result()

Looks up the status of a direct debit mandate.

Required attributes

  • "input_CustomerMSISDN"
  • "input_ThirdPartyReference"
  • "input_ThirdPartyConversationID" - optional; generated when omitted.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.query_direct_debit(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.query_direct_debit(attrs, market: :ghana)

query_direct_debit!(attrs, opts \\ [])

@spec query_direct_debit!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as query_direct_debit/2 but returns the response directly and raises ElixirMpesa.Error on failure.

query_transaction_status(attrs, opts \\ [])

@spec query_transaction_status(
  attrs(),
  keyword()
) :: result()

Looks up the status of a transaction.

Required attributes

  • "input_QueryReference"
  • "input_ThirdPartyConversationID" - optional; generated when omitted.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.query_transaction_status(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.query_transaction_status(attrs, market: :ghana)

query_transaction_status!(attrs, opts \\ [])

@spec query_transaction_status!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as query_transaction_status/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Direct debit

direct_debit_cancel(attrs, opts \\ [])

@spec direct_debit_cancel(
  attrs(),
  keyword()
) :: result()

Cancels a direct debit mandate.

Required attributes

  • "input_CustomerMSISDN"
  • "input_ThirdPartyReference"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.direct_debit_cancel(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.direct_debit_cancel(attrs, market: :ghana)

direct_debit_cancel!(attrs, opts \\ [])

@spec direct_debit_cancel!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as direct_debit_cancel/2 but returns the response directly and raises ElixirMpesa.Error on failure.

direct_debit_creation(attrs, opts \\ [])

@spec direct_debit_creation(
  attrs(),
  keyword()
) :: result()

Creates a direct debit mandate against a customer.

Required attributes

  • "input_AgreedTC"
  • "input_CustomerMSISDN"
  • "input_ThirdPartyReference"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.direct_debit_creation(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.direct_debit_creation(attrs, market: :ghana)

direct_debit_creation!(attrs, opts \\ [])

@spec direct_debit_creation!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as direct_debit_creation/2 but returns the response directly and raises ElixirMpesa.Error on failure.

direct_debit_payment(attrs, opts \\ [])

@spec direct_debit_payment(
  attrs(),
  keyword()
) :: result()

Collects a payment against an existing mandate.

Required attributes

  • "input_Amount"
  • "input_CustomerMSISDN"
  • "input_ThirdPartyReference"
  • "input_ThirdPartyConversationID" - unique per transaction; see conversation_id/0. Required.

"input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted.

Options

  • :market, :api_type, :url_context, :country, :currency, :service_provider_code, :api_key, :public_key - override configuration for this call.
  • :cache - set to false to bypass the session cache.

Examples

{:ok, response} = ElixirMpesa.direct_debit_payment(attrs)
response.transaction_id

# Against a different market, for one call only
ElixirMpesa.direct_debit_payment(attrs, market: :ghana)

direct_debit_payment!(attrs, opts \\ [])

@spec direct_debit_payment!(
  attrs(),
  keyword()
) :: ElixirMpesa.Response.t()

Same as direct_debit_payment/2 but returns the response directly and raises ElixirMpesa.Error on failure.

Session

conversation_id()

@spec conversation_id() :: conversation_id()

Generates a unique conversation ID suitable for "input_ThirdPartyConversationID".

Use one per logical transaction, and reuse the same value when retrying that transaction so M-Pesa can recognise the duplicate.

Examples

iex> id = ElixirMpesa.conversation_id()
iex> String.length(id)
32

iex> ElixirMpesa.conversation_id() != ElixirMpesa.conversation_id()
true

refresh_session(opts \\ [])

@spec refresh_session(keyword()) :: :ok | {:error, ElixirMpesa.Error.t()}

Discards the cached session so the next call re-authenticates.

Examples

:ok = ElixirMpesa.refresh_session(market: :tanzania)

session_key(opts \\ [])

@spec session_key(keyword()) :: {:ok, String.t()} | {:error, ElixirMpesa.Error.t()}

Returns a valid encrypted session key, obtaining and caching one if needed.

You rarely need this — every operation calls it for you. It is here for callers doing something the library does not cover, such as an endpoint added after this release.

Examples

{:ok, session_key} = ElixirMpesa.session_key()
{:ok, session_key} = ElixirMpesa.session_key(market: :ghana)

Types

attrs()

@type attrs() :: %{optional(String.t()) => term()}

Request attributes, using M-Pesa's own input_* key names.

conversation_id()

@type conversation_id() :: String.t()

A unique identifier for one logical transaction. See conversation_id/0.

market()

@type market() :: ElixirMpesa.Config.market()

A market with a built-in preset. See ElixirMpesa.Config.markets/0.

msisdn()

@type msisdn() :: String.t()

A customer phone number in international format without a +, e.g. "255700000000".

result()

@type result() :: {:ok, ElixirMpesa.Response.t()} | {:error, ElixirMpesa.Error.t()}