Authentication
View SourceWhat the credentials are
| Credential | Secret? | What it is |
|---|---|---|
| API key | Yes | Identifies your application. Issued per market and per environment. |
| Public key | No | The market's RSA public key. Your API key is encrypted with it. |
| Service provider code | No | Your business shortcode. |
The public key is genuinely public — it is published in the developer portal, and there is no harm in committing it. The API key is a bearer credential: anyone holding it can move money on your account.
Where to put them
Use config/runtime.exs and read from the environment. config/config.exs is evaluated
at compile time and is committed to your repository — credentials placed there end up in
your git history and inside your release artefacts.
# config/runtime.exs
import Config
if config_env() == :prod do
config :elixir_mpesa,
api_type: "openapi",
market: :tanzania,
service_provider_code: System.fetch_env!("MPESA_SERVICE_PROVIDER_CODE"),
api_key: System.fetch_env!("MPESA_API_KEY"),
public_key: System.fetch_env!("MPESA_PUBLIC_KEY")
endSystem.fetch_env!/1 raises at boot if the variable is missing, which is what you want —
better a failed deploy than an application that starts and fails on the first payment.
If you would rather not use the application environment at all, every option can be passed per call:
ElixirMpesa.c2b(attrs, api_key: key, public_key: pub, market: :tanzania)This is useful when credentials live in a vault, or differ per tenant.
How the handshake works
The OpenAPI does not accept your API key directly. It requires a session key, obtained in three steps:
- Encrypt the API key with the market's RSA public key (PKCS#1 v1.5), base64-encoded.
- Exchange it at
GET /getSession/, sending it as a bearer token. M-Pesa replies with anoutput_SessionID. - Encrypt the session ID the same way. That is the bearer token for every other call.
Step 3 catches people out: the raw session ID is not the token — the encrypted session ID is.
What this library does for you
ElixirMpesa.Session performs all three steps on first use, then:
- Caches the encrypted session key per
{api_type, market}, so several markets can be served at once without interfering. - Refreshes early, at 80% of the one-hour lifetime, so a request in flight never races the expiry boundary.
- Collapses concurrent misses — a hundred simultaneous requests on a cold cache
produce one
getSessioncall, not a hundred. - Re-authenticates once if M-Pesa rejects the session key mid-flight, then retries.
So in normal use you never touch any of it:
ElixirMpesa.c2b(attrs)Managing sessions yourself
If you need the session key — for an endpoint this library does not cover yet, say:
{:ok, session_key} = ElixirMpesa.session_key()
{:ok, session_key} = ElixirMpesa.session_key(market: :ghana)To force re-authentication:
:ok = ElixirMpesa.refresh_session()To bypass the cache entirely for one call:
ElixirMpesa.c2b(attrs, cache: false)The cache degrades gracefully: if ElixirMpesa.Session is not running — for instance
when the library is used without its application started — every call falls back to an
uncached fetch rather than failing.
Transport security
Every request verifies the server's TLS certificate against the system trust store and
checks the hostname. This is not configurable, and it is worth stating plainly because
version 0.1.0 of this library disabled it — it passed hackney: [:insecure] on every
call, which meant the encrypted API key and bearer session token travelled over
connections that were never authenticated. If you are on 0.1.0, upgrade.
See Upgrading.