Recipe: payroll and payouts
End-to-end recipe for paying people from a business with M-Pesa B2C, such as payroll, commissions, supplier-to-wallet payments, winnings and refunds.
When your agent uses it: End-to-end recipe for paying people from a business with M-Pesa B2C, such as payroll, commissions, supplier-to-wallet payments, winnings and refunds. Covers a payout batch table, idempotency keys, approvals, recipient allowlists, handling queue timeouts without double-paying, result callbacks, and float checks. Use when someone wants to send money to many phones, run salaries or disbursements, or build a refund flow.
Install only this skill:
npx skills add Mr-Macharia/mpesa-daraja-skills-and-mcp --skill daraja-payouts
Money leaves the business, so mistakes are expensive. Every payout gets a key, an approval rule and a confirmed result.
Needs
| Need | Default |
|---|---|
| Who gets paid, and how much | a CSV or table: phone, amount, reference |
| B2C shortcode, initiator name, SecurityCredential | from the Daraja portal (daraja-credentials-security) |
| CommandID | BusinessPayment (SalaryPayment for salaries) |
| Who approves, and above what amount | one approver for each batch; any single payout above KES 10,000 also asks |
| Result and timeout URLs | your HTTPS domain, or the MCP receiver |
Work these out before asking
- Is there an existing employee, supplier or customer table with phones? Use it as the source.
mpesa_setup_status(MCP) or.env: areDARAJA_INITIATOR_NAMEandDARAJA_SECURITY_CREDENTIALset?- Is it a refund? Then you need the original receipt for each row (
refund_of).
Ask at most three questions (who approves, the per-payout limit, the CommandID), each with the default above.
Build
- Tables.
- A
payout_batchesrow (draft → approved → sent → done). - One
paymentsrow for each line (direction = 'out'), withidempotency_key = <batch>-<line>. Use the ledger fromdaraja-payment-design.
- A
- Validate the batch before anyone approves it:
- phones normalised to
2547XXXXXXXX - amounts whole shillings and at least KES 10
- no duplicate phone and amount in one batch
- the total is within the current balance (
daraja-account-ops, Account Balance)
- phones normalised to
- Approve. A person approves the batch, and the total is shown. With the MCP server, set:
DARAJA_APPROVAL_THRESHOLDfor per-payout promptsDARAJA_ALLOWED_RECIPIENTSwhen the recipients are fixed, e.g. payroll
- Send each line with B2C (
daraja-b2c).- Generate and store the
OriginatorConversationIDbefore the call, and pace requests (a few per second). ResponseCode "0"→acceptedonly.
- Generate and store the
- Results.
- The ResultURL callback with
ResultCode0 →confirmed, and recordTransactionID. - A non-zero code →
failed, with the reason. - A QueueTimeOutURL hit or a network error →
timed_out. Then run Transaction Status. Never resend.
- The ResultURL callback with
- Retry only
failedlines, as a new attempt with a new key, after fixing the cause (wrong number, limit). - Close the batch when every line is final. Report paid, failed and unknown, and the balance before and after.
With the MCP server: mpesa_b2c_payment (the key is required), then mpesa_get_result(key). Or from a terminal: mpesa pay b2c 07XXXXXXXX 500 --key payroll-2026-10-07.
Done when
- Re-running a whole batch sends nothing twice
- A forced timeout ends in
timed_out, then a status query, with no second payment - A payout above the limit cannot go out without a human yes
- Every line is reconciled against the B2C result and the account balance
Related: daraja-b2c, daraja-account-ops, daraja-credentials-security, daraja-payment-design.