Authentication

View Source

What the credentials are

CredentialSecret?What it is
API keyYesIdentifies your application. Issued per market and per environment.
Public keyNoThe market's RSA public key. Your API key is encrypted with it.
Service provider codeNoYour 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")
end

System.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:

  1. Encrypt the API key with the market's RSA public key (PKCS#1 v1.5), base64-encoded.
  2. Exchange it at GET /getSession/, sending it as a bearer token. M-Pesa replies with an output_SessionID.
  3. 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 getSession call, 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.