PayHero Integration Reference¶
Source: PayHero API documentation (API 2.0.0) Owner: treasury-api Updated: October 2026
PayHero Africa is treasury's gateway for M-Pesa collections into a tenant's own paybills, tills and bank accounts, for mobile money in other countries (MTN, Airtel and other networks), for card hosted checkout, bank deposits and the offline paybill, and for payouts from a tenant's PayHero wallet. Only the 2.0.0 API is used.
See Payment Workflow for the end-to-end flow every gateway shares (intent
creation, the shared pay page, initiate_url, callbacks). This page covers what is specific to
PayHero. For the tenant-facing setup steps, see the
PayHero user guide.
Hosts and authentication¶
| Host | Used for |
|---|---|
https://api.payhero.africa |
Payments, rail discovery, status, balance, beneficiaries, payment channels |
https://auth.payhero.africa |
Teams (accounts), invites, KYC |
Both use HTTP Basic auth with the API username and password. The platform's key is stored encrypted in treasury's gateway configuration and is never exposed to tenants.
Account modes¶
The platform owner configures PayHero once (Platform, Gateways). Each tenant then turns PayHero on in one of three modes:
| Mode | Account used | Own wallet | Notes |
|---|---|---|---|
platform_team (default) |
The tenant's own Team inside the platform's PayHero organization | Yes, isolated per Team | The only mode that supports escrow; needed for wallet collections |
platform_root |
The platform's root account; the platform owner assigns specific channels to the tenant | No | Channel collections only |
own_account |
The tenant's own PayHero API key (stored encrypted) | Its own | The account is detected from the key's channels on the first sync; no escrow |
A platform_team tenant without a Team has no account at all. It never falls back to the
platform's account, so its collections and payouts can never touch the platform's wallet.
Creating Teams requires a PayHero enterprise organization. On other plans PayHero refuses Team creation; treasury explains the options: upgrade the organization with PayHero, use the shared platform account mode, connect the tenant's own PayHero account, or link a Team created on the PayHero dashboard.
PayHero has no API to create channels or wallets. A Team gets its wallet (and a service wallet that pays PayHero's own costs) when it is created. Paybills, tills and bank accounts are added on the PayHero dashboard by someone invited into the Team, then synced into treasury. A collection straight to a channel needs credit in the service wallet (with an empty one PayHero refuses it); wallet deposits and payouts take PayHero's charge from the money instead (see Fees and collection routes).
Who sets what¶
| Setting | Level | Where |
|---|---|---|
| API key, organization and root account | Platform | Platform, Gateways, PayHero (Detect fills the ids) |
| Gateway available to tenants, platform primary | Platform | Platform, Gateways |
| PayHero on, mode, country, offline paybill | Tenant | Settings, Payments, PayHero, Account |
| Team (create or link) | Tenant, or the platform owner on their behalf | Settings, Payments, PayHero, Account |
| Channels on or off, routing, payment links | Tenant | Settings, Payments, PayHero, Channels and routing |
| KYC verification | Tenant | PayHero dashboard (the Team, Management, Verification); treasury reads the tier back |
| Which gateways customers see, tenant primary | Tenant | Settings, Payments, Gateways |
Channels and routing¶
Treasury syncs a tenant's channels on demand and every 15 minutes, and keeps an on/off switch per channel. Each collection is routed to a channel in this order:
- the outlet override,
- the payment's reference type (for example
invoiceorpos_order), - the default channel (the first active channel when none is set).
The routing screen only lists payment types the tenant can actually receive: those of the products it subscribes to, those it received in the last 180 days, and those already routed.
A Kenyan M-Pesa pay-in reaches the routed channel straight, or relayed through a payments wallet (see Fees and collection routes). Escrow contributions and wallet top-ups carry no channel and land in the Team wallet.
Payments to the platform¶
Plan subscriptions, renewals, support-agreement charges, top-ups and card setup are charged on the platform's own PayHero account and follow its routing, whatever gateway the paying tenant uses; the pay page lists the platform's methods for them. A payment made from an invoice's Pay Now page takes the invoice's own routing (a subscription invoice routes as a subscription, not as a manual invoice).
Personal channels (platform owner)¶
The platform owner can mark a channel as a personal account with a payee name. It never maps to a company account, never takes a business route and is the only channel personal collections (support agreements set to Personal) settle into, by M-Pesa only. Those invoices are kept off the company's books entirely: no ledger entry, receivable, eTIMS submission or report figure.
Plans¶
PayHero is gated on the plan's mpesa_integration feature (from the Growth tier, like direct
M-Pesa): without it the PayHero write routes answer 403, the pay page offers no PayHero method and
initiation refuses. Payments to the platform are not gated.
Shared-account tenants¶
A tenant in platform_root mode collects into its own paybill or till added on the platform's
PayHero account. The platform owner assigns the channel to it
(POST /platform/gateways/payhero/channels/{id}/assign); tenants cannot claim channels. The
platform never lists, routes to or maps an assigned channel, and the tenant's collections are its
own (its intent, its ledger, never platform revenue or a payout owed). Its payments are always
relayed through the platform's wallet and paid on to its channel, so PayHero's charge comes out of
each payment and nothing is billed to the tenant later.
Fees and collection routes¶
PayHero charges in two places:
- Channel collections (straight into a paybill, till or bank): a flat fee per amount band from
PayHero's published tariff (
/api/transaction_fees, mirrored daily into the platform fee rules for gatewaypayhero), taken from the account's service wallet. - Wallet deposits and payouts: a charge kept out of the money itself; the service wallet is
never used. PayHero does not publish these charges. It reports them on each line of the
account's ledger (
GET /api/v2/transactions, fieldcost).
Each tenant picks a collection route (collection_route on PUT /{tenant}/gateways/payhero):
| Route | Behaviour |
|---|---|
auto (default) |
The channel while the service wallet covers the band fee, unless a relay is measured to be cheaper at this amount; a relay when the service wallet is short. |
relay |
Always collect into a payments wallet and pay the channel on. |
channel |
Always straight to the channel; refused while the service wallet is empty. |
A shared-account tenant always relays. The relay is carried by the tenant's own wallet when
PayHero lets it prompt customers into it (own PayHero account, or a Team at KYC tier 3), otherwise
by the platform's root wallet (platform setting payhero.relay_platform_carrier, on by default),
which pays the tenant's channel straight on. Escrow, platform billing, personal and offline
payments are never relayed.
Relays are priced from a table of charges PayHero really made, read back from the carrier's
ledger after every relay (job "payhero relay costs", every 15 minutes) and stored once per fact:
a charge already stated by the table (the same amount, or an amount inside a range whose ends
already predict it) is not stored again. auto treats a relay as cheaper only when its charges
were measured near the amount.
Who pays: the tenant's fee_bearer (default payer). With payer the fee is added to the prompt
(quote: GET /pay/{tenant}/fees/payhero, which runs the same route choice) and booked as a
recovered charge (4600); on a relay the surcharge is the relay's, and the channel receives exactly
the price. With merchant the payer is prompted for the price; on a relay the channel receives
the price less PayHero's charge. A relay's charge is booked Dr 5100 M-Pesa Transaction Fees / Cr
the account the payment settled into, and corrected to PayHero's real charge once it is read when
the tenant's own wallet carried it. Nothing is billed to a tenant afterwards. Payments to the
platform, escrow and personal collections are never surcharged.
What customers are offered¶
PayHero is its own gateway, separate from Daraja M-Pesa. GET /api/v1/pay/{tenant}/gateways
lists payhero only when the tenant's account can take a payment (it has a Team or its own
account), with its rails in payhero_methods:
{"gateways":["paystack","payhero","cod"],"payhero_methods":["mpesa"],"providers":{}}
- The rails come from PayHero's discovery for the payment's country (M-Pesa, Airtel, MTN, other networks, card, bank). If discovery is unavailable, M-Pesa alone is offered.
- Only Kenyan M-Pesa is offered until the platform service config
payhero.global_rails_verifiedistrue. Every other rail runs onPOST /api/global/payments, which has not been seen to settle: live discovery (2026-10-07) lists deposit networks only, no withdraw network in any country, and an emptymerchant_idthat its provider (bitpay) requires. Kenyan M-Pesa collections and M-Pesa, paybill and till payouts use the V1 endpoints and are proven live. Bank, Airtel and cross-border payouts are refused before PayHero is called. mpesaingatewaysmeans Daraja only (the business's own paybill or till).- The offline paybill (
payhero_offline) is offered only when the business switched it on and the platform has confirmed it with a live payment (payhero.offline_paybill_verified).
The payment page shows one PayHero option that opens a checkout listing these rails. Each rail is
started with gateway: "payhero" on the initiate call, which pins PayHero: the payment never falls
back to the business's Daraja account. The POS shows one PayHero tender; STK Push and C2B (matching
a payment the customer already made to the till) are Daraja only.
Collections¶
| Rail | Endpoint |
|---|---|
| Kenyan M-Pesa | POST /api/v2/payments with the routed channel (or the Team wallet) |
| Every other pay-in (other countries, Airtel, MTN, card, bank) | POST /api/global/payments |
| Offline paybill | V1 collection with is_offline: true |
Country and currency. The payment's currency picks the country's rails (KES on Kenya's, UGX on Uganda's, and so on); the tenant's configured country is the fallback and also drives which networks the pay page lists. Each rail charges in its own country's currency, so a payment in another currency (say a USD invoice paid by M-Pesa) is converted at the stored exchange rate and rounded up to whole units. The conversion is kept on the payment intent, settlement checks the amount against it, and the ledger still posts the intent's own currency and amount. With no rate available the payment is refused rather than charged in the wrong currency.
Offline paybill. For payers who cannot take a phone prompt. The pay page shows PayHero's paybill and an account number; the payer pays from the M-Pesa menu and the payment settles like any other when PayHero's callback arrives.
Prompt guard. Every gateway that sends a prompt to the payer's phone goes through one guard: after repeated failed or cancelled prompts to the same phone (or many for the same tenant) in a short window, further prompts pause and the payer is pointed to the offline paybill. A successful payment clears the count for that phone.
Where the money lands in the books¶
Each synced channel maps to one of the tenant's financial accounts (Banking, Accounts). On sync, an unmapped channel is matched by account number, or to the only account at the bank named in the channel; the tenant can change it by hand. The Team wallet maps to an account too. When a collection settles, its ledger entry debits the mapped account, so each account's balance follows the money.
Each payment is booked once: a gateway payment of an invoice posts one receipt per payment intent (enforced in the database), the invoice's paid amount changes under a row lock once per intent, and voiding a manual payment removes only that payment.
Callbacks¶
POST /api/v1/webhooks/payhero. PayHero callbacks are not signed, so a callback is only treated
as a hint: treasury looks the payment up with PayHero's transaction status endpoint before settling
it through the single settlement path. Payout callbacks are confirmed the same way, and a payout
whose callback never arrives is looked up again after 15 minutes.
Payments made on a PayHero dashboard Payment Link have no treasury intent and cannot be attributed to a tenant from the callback, so they are logged only. Tenants can still save their Payment Link and Hosted Checkout URLs in treasury to share them.
Payouts and wallet withdrawals¶
Kenyan phones, paybills and tills are paid out through V1 withdraw; everything else through the global withdrawal endpoint. Payouts come from the tenant's own account and follow the tenant's payout approval policy; the one exception is a relay the platform's wallet carried, which the platform pays on to the tenant's channel (it shows in the tenant's withdrawal history).
A tenant moves money out of its Team wallet with POST .../wallet/withdraw to one of its own
channels or a phone. The disbursement approval policy applies (the request returns
409 approval_required until approved). When the payout succeeds, the amount moves between the
mapped accounts in the books. PayHero takes the withdrawal charge from the wallet itself.
KYC¶
PayHero's tiers are 1 Personal, 2 Business Lite, 3 Company and 4 Cross-border. Collecting from
the public into a Team wallet, and escrow, need tier 3. Verification is done on the PayHero
dashboard (the Team, Management, Verification). Treasury only reads the tier and status back with
the platform organization key, on demand (POST .../kyc/refresh) and daily for every Team.
API routes¶
Tenant routes live under /{tenant}/gateways/payhero (reads need treasury.gateways.view,
writes treasury.gateways.manage):
| Method | Path | Purpose |
|---|---|---|
| GET | `` | Status: mode, Team, KYC, channels, routing, links, escrow terms, reconciliation |
| PUT / DELETE | `` | Enable or change mode / disable |
| POST | /team, /team/link, /team/invite |
Create the Team; link an existing Team; invite an admin |
| POST | /channels/sync |
Sync channels (platform_root tenants keep only their assigned channels) |
| PATCH | /channels/{id} |
Switch a channel on or off |
| PUT | /channels/{id}/personal |
Mark a channel personal or business (platform tenant only) |
| PUT | /channels/{id}/account, /wallet-account |
Map a channel or the wallet to a financial account |
| PUT | /routing, /payment-links |
Channel routing; saved Payment Links |
| GET | /routing/options |
Payment types this tenant can route |
| GET | /balance, /discovery?country= |
Team wallet balance; rails for a country |
| POST / GET | /wallet/withdraw, /wallet/withdrawals |
Withdraw from the Team wallet; recent withdrawals |
| POST | /kyc/refresh |
Read the Team's KYC tier back from PayHero |
| POST | /service-wallet/topup |
M-Pesa prompt that tops up the account's service wallet |
Platform routes, under /platform/gateways/payhero:
| Method | Path | Purpose |
|---|---|---|
| GET / PUT | /settings |
Organization, root account and fee bearer |
| POST | /settings/detect |
Fill the organization and root account ids from the key |
| POST | /service-wallet/topup |
Top up the root account's service wallet |
| GET | /teams |
Every tenant's Team (?balances=true adds wallet balances) |
| POST | /teams/{tenantID}, /teams/{tenantID}/link |
Create or link a Team for a tenant |
| GET | /channels |
Root-account channels and the tenant each is assigned to |
| POST / DELETE | /channels/{channelID}/assign |
Assign a root-account channel to a platform_root tenant, or take it back |
| GET | /tariff |
PayHero's published channel tariff |
| POST | /tariff/sync |
Mirror the tariff into the fee rules |
Background jobs¶
| Job | Every | Purpose |
|---|---|---|
| Channel sync | 15 minutes | Keep every tenant's channels current |
| KYC sync | 24 hours | Refresh Team KYC tiers |
| Tariff sync | 24 hours | Mirror PayHero's channel tariff into the fee rules |
Each runs once per period across all treasury replicas.