Payments

View Source

Four operations move money: c2b/2, b2c/2, b2b/2 and reversal/2. Each has a ! variant that returns the response directly and raises on failure.

Throughout, "input_Country", "input_Currency" and "input_ServiceProviderCode" are filled in from configuration when omitted — the examples below pass only what varies.

Idempotency — read this first

"input_ThirdPartyConversationID" is the idempotency key.

  • Unique per logical transaction. ElixirMpesa.conversation_id/0 generates one.
  • Identical across retries of that transaction. M-Pesa uses it to recognise a duplicate and reject it rather than processing it twice.

This library therefore:

  • never retries a payment automatically — a gateway timeout on a payment often means the payment went through and only the response was lost; and
  • refuses to generate a conversation ID for a payment, because a generated one would give each retry a fresh key and defeat the deduplication.

Read-only queries do get one generated, since a duplicate query is harmless.

Store the conversation ID with your order before sending, so a crash mid-request does not lose it:

conversation_id = ElixirMpesa.conversation_id()
{:ok, order} = Orders.record_attempt(order, conversation_id)

ElixirMpesa.c2b(%{
  "input_ThirdPartyConversationID" => conversation_id,
  ...
})

C2B — customer pays you

The most common operation. Charges a customer's M-Pesa wallet and credits your business.

ElixirMpesa.c2b(%{
  "input_Amount" => "10",
  "input_CustomerMSISDN" => "255700000000",
  "input_TransactionReference" => "INV-1024",
  "input_ThirdPartyConversationID" => ElixirMpesa.conversation_id(),
  "input_PurchasedItemsDesc" => "Order 1024"
})

The customer is prompted on their handset to authorise the payment, so the call can take several seconds. It returns when they respond or the prompt expires.

AttributeRequiredNotes
"input_Amount"YesAs a string.
"input_CustomerMSISDN"YesInternational format, no +. See Markets.
"input_TransactionReference"YesYour reference, shown to the customer.
"input_ThirdPartyConversationID"YesSee above.
"input_PurchasedItemsDesc"YesDescription of what is being bought.

B2C — you pay a customer

Disbursements, refunds, payouts, salaries.

ElixirMpesa.b2c(%{
  "input_Amount" => "10",
  "input_CustomerMSISDN" => "255700000000",
  "input_TransactionReference" => "REFUND-1024",
  "input_ThirdPartyConversationID" => ElixirMpesa.conversation_id(),
  "input_PaymentItemsDesc" => "Refund for order 1024"
})

Note "input_PaymentItemsDesc" here, where C2B uses "input_PurchasedItemsDesc". This asymmetry is M-Pesa's, not this library's.

Your business account must be funded. An unfunded payout fails at the API.

B2B — you pay another business

ElixirMpesa.b2b(%{
  "input_Amount" => "1000",
  "input_PrimaryPartyCode" => "000000",
  "input_ReceiverPartyCode" => "000001",
  "input_TransactionReference" => "PO-88",
  "input_ThirdPartyConversationID" => ElixirMpesa.conversation_id(),
  "input_PurchasedItemsDesc" => "Purchase order 88"
})

B2B addresses businesses by shortcode rather than phone number, so there is no "input_CustomerMSISDN".

Reversal — undo a transaction

ElixirMpesa.reversal(%{
  "input_TransactionID" => "49XCD123F6",
  "input_ReversalAmount" => "10",
  "input_ThirdPartyConversationID" => ElixirMpesa.conversation_id()
})

"input_TransactionID" is M-Pesa's ID from the original response (response.transaction_id), not your own reference. Reversals are time-limited and may require permissions your account does not have by default — check with your account manager before relying on them.

In 0.1.0 this operation existed but was never exposed on the top-level module and was absent from the README. It is available now.

Checking a payment

Never infer an outcome from a transport failure. If a payment errors with a :transport or :http category, the transaction may still have succeeded — ask:

ElixirMpesa.query_transaction_status(%{"input_QueryReference" => "INV-1024"})

See Error codes for which failures are ambiguous.