Payments
View SourceFour 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/0generates 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.
| Attribute | Required | Notes |
|---|---|---|
"input_Amount" | Yes | As a string. |
"input_CustomerMSISDN" | Yes | International format, no +. See Markets. |
"input_TransactionReference" | Yes | Your reference, shown to the customer. |
"input_ThirdPartyConversationID" | Yes | See above. |
"input_PurchasedItemsDesc" | Yes | Description 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.