Packages

Vodacom M-Pesa OpenAPI client for Elixir — C2B, B2C, B2B, reversals, direct debit and transaction queries across Tanzania, Lesotho, Ghana and the DRC.

Current section

Files

Jump to
elixir_mpesa README.md
Raw

README.md

# ElixirMpesa
[![Hex.pm](https://img.shields.io/hexpm/v/elixir_mpesa.svg)](https://hex.pm/packages/elixir_mpesa)
[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/elixir_mpesa)
[![CI](https://github.com/jamesnjovu/elixir_mpesa/actions/workflows/ci.yml/badge.svg)](https://github.com/jamesnjovu/elixir_mpesa/actions/workflows/ci.yml)
[![Downloads](https://img.shields.io/hexpm/dt/elixir_mpesa.svg)](https://hex.pm/packages/elixir_mpesa)
[![License](https://img.shields.io/hexpm/l/elixir_mpesa.svg)](https://github.com/jamesnjovu/elixir_mpesa/blob/main/LICENSE)
**M-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.
```elixir
{: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
```elixir
def deps do
[{:elixir_mpesa, "~> 0.2.0"}]
end
```
Requires **Elixir 1.15+** and OTP 25+.
## Configuration
Credentials are secrets — put them in `config/runtime.exs`, read from the environment.
```elixir
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](https://openapiportal.m-pesa.com).
## 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](https://hexdocs.pm/elixir_mpesa/markets.html).
## 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](https://hexdocs.pm/elixir_mpesa/authentication.html).
## Error handling
Every function returns `{:ok, ElixirMpesa.Response.t()}` or
`{:error, ElixirMpesa.Error.t()}`. Match on `reason` and `category`:
```elixir
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()
end
```
See [Error codes](https://hexdocs.pm/elixir_mpesa/error-codes.html).
## 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](https://hexdocs.pm/req), so your suite runs against a stub with no network:
```elixir
config :elixir_mpesa, req_options: [plug: {Req.Test, ElixirMpesa.Client}]
```
```elixir
Req.Test.stub(ElixirMpesa.Client, fn conn ->
Req.Test.json(conn, %{"output_ResponseCode" => "INS-0", "output_TransactionID" => "TX1"})
end)
```
See the [Testing guide](https://hexdocs.pm/elixir_mpesa/testing.html).
## Documentation
- [Getting started](https://hexdocs.pm/elixir_mpesa/getting-started.html)
- [Markets](https://hexdocs.pm/elixir_mpesa/markets.html)
- [Authentication](https://hexdocs.pm/elixir_mpesa/authentication.html)
- [Payments](https://hexdocs.pm/elixir_mpesa/payments.html)
- [Direct debit](https://hexdocs.pm/elixir_mpesa/direct-debit.html)
- [Error codes](https://hexdocs.pm/elixir_mpesa/error-codes.html)
- [Testing](https://hexdocs.pm/elixir_mpesa/testing.html)
- [Upgrading from 0.1.0](https://hexdocs.pm/elixir_mpesa/upgrading.html)
Using an AI coding assistant? [`llms.txt`](https://hexdocs.pm/elixir_mpesa/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](https://hexdocs.pm/elixir_mpesa/upgrading.html).
## Contributing
Issues and pull requests welcome at
[github.com/jamesnjovu/elixir_mpesa](https://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.
```bash
mix deps.get
mix test # or: mix ci — format, credo, dialyzer and tests
```
## License
MIT — see [LICENSE](LICENSE).