Error codes

View Source

Every failure arrives as an ElixirMpesa.Error exception struct, whatever its origin.

{:error, %ElixirMpesa.Error{
  reason: :insufficient_balance,   # a documented atom — match on this
  category: :api,                  # where the failure came from
  code: "INS-2006",                # M-Pesa's raw response code, when there is one
  message: "...",                  # M-Pesa's own description
  status: 400,                     # HTTP status, when there was a response
  operation: :c2b,                 # which call failed
  raw: %{...}                      # the complete decoded body
}}

Categories

Match on :category to decide how to react; the category tells you whether the transaction definitely did not happen, or whether you cannot tell.

CategoryMeaningDid the transaction happen?
:configYour configuration or attributes are wrong. No request was sent.No
:cryptoThe public key or the value being encrypted is invalid. No request was sent.No
:transportThe request never completed — timeout, DNS, refused connection.Unknown
:httpA response arrived, but not one M-Pesa's API describes.Unknown
:apiM-Pesa answered with a response code.No — it told you it declined

The distinction that matters in production is between :api and everything ambiguous.

case ElixirMpesa.c2b(attrs) do
  {:ok, response} ->
    confirm(response)

  # M-Pesa gave a definite answer. The money did not move.
  {:error, %ElixirMpesa.Error{category: :api} = error} ->
    decline(error.code)

  # Your bug. Fix it, do not retry.
  {:error, %ElixirMpesa.Error{category: category} = error} when category in [:config, :crypto] ->
    raise error

  # Outcome unknown. Do NOT resend blindly — query the status first.
  {:error, %ElixirMpesa.Error{}} ->
    reconcile_later(attrs["input_ThirdPartyConversationID"])
end

Reasons

:config and :crypto

ReasonMeaning
:missing_configA required option or attribute was not supplied. The message names it.
:invalid_configA value is present but wrong — an unknown api_type, or an unreplaced placeholder.
:unknown_market:market is not one this library knows. See Markets.
:invalid_public_keyThe public key is not valid base64, or not a DER RSA key.
:payload_too_largeMore than 501 bytes given to a 4096-bit PKCS#1 encryption.
:encryption_failedRSA encryption failed for another reason.

:transport

ReasonMeaning
:timeoutNo response within 30 seconds.
:closedM-Pesa closed the connection before responding.
:nxdomainopenapi.m-pesa.com did not resolve.
:econnrefusedThe connection was refused.
:transport_errorAny other transport failure.

:http

ReasonStatusMeaning
:unauthorized401Session key rejected. The library retries once with a fresh session before surfacing this.
:forbidden403Credentials lack permission for this operation.
:not_found404No such endpoint — check api_type and url_context.
:rate_limited429Slow down.
:server_error5xxM-Pesa-side failure.
:unexpected_statusotherAnything else.
:invalid_jsonanyThe body was not a JSON object — usually a gateway HTML error page.

Version 0.1.0 enumerated ten specific statuses and raised FunctionClauseError on anything else, so a 403, 404, 429, 502 or 504 crashed the caller instead of returning an error tuple. Every status is handled now.

M-Pesa response codes (INS-*)

INS-0 means success. Any other code is a decline, and arrives as category: :api with the code in :code and M-Pesa's own description in :message.

{:error, %ElixirMpesa.Error{code: "INS-2006"} = error} ->
  Logger.warning(Exception.message(error))

Why there is no lookup table here

Vodacom documents the INS-* codes only inside the authenticated developer portal. They are not published anywhere public, and they vary somewhat between markets.

This library therefore does not guess at their meanings. Rather than ship a table of plausible-looking but unverified mappings — which would be worse than none, because you would trust it — unrecognised codes get reason: :unknown, and the raw :code and M-Pesa's own :message are preserved so you can match on them exactly:

{:error, %ElixirMpesa.Error{code: code}} when code in ~w(INS-2006 INS-2051) ->
  ...

error.message is whatever M-Pesa put in output_ResponseDesc, verbatim. In practice that description is the authoritative meaning, and it is already localised to the market you are calling.

If you have portal documentation for these codes, a pull request adding them to @code_reasons in ElixirMpesa.Error would be genuinely valuable — it is the one piece of this library that cannot be built from public sources.

Raising instead of matching

Every function has a ! variant that returns the response and raises the error:

response = ElixirMpesa.c2b!(attrs)

Since ElixirMpesa.Error is an exception, it works with Exception.message/1, is rescuable, and prints usefully in logs and error reporters.