Error codes
View SourceEvery 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.
| Category | Meaning | Did the transaction happen? |
|---|---|---|
:config | Your configuration or attributes are wrong. No request was sent. | No |
:crypto | The public key or the value being encrypted is invalid. No request was sent. | No |
:transport | The request never completed — timeout, DNS, refused connection. | Unknown |
:http | A response arrived, but not one M-Pesa's API describes. | Unknown |
:api | M-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"])
endReasons
:config and :crypto
| Reason | Meaning |
|---|---|
:missing_config | A required option or attribute was not supplied. The message names it. |
:invalid_config | A 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_key | The public key is not valid base64, or not a DER RSA key. |
:payload_too_large | More than 501 bytes given to a 4096-bit PKCS#1 encryption. |
:encryption_failed | RSA encryption failed for another reason. |
:transport
| Reason | Meaning |
|---|---|
:timeout | No response within 30 seconds. |
:closed | M-Pesa closed the connection before responding. |
:nxdomain | openapi.m-pesa.com did not resolve. |
:econnrefused | The connection was refused. |
:transport_error | Any other transport failure. |
:http
| Reason | Status | Meaning |
|---|---|---|
:unauthorized | 401 | Session key rejected. The library retries once with a fresh session before surfacing this. |
:forbidden | 403 | Credentials lack permission for this operation. |
:not_found | 404 | No such endpoint — check api_type and url_context. |
:rate_limited | 429 | Slow down. |
:server_error | 5xx | M-Pesa-side failure. |
:unexpected_status | other | Anything else. |
:invalid_json | any | The body was not a JSON object — usually a gateway HTML error page. |
Version 0.1.0 enumerated ten specific statuses and raised
FunctionClauseErroron 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.