Payment Channel collections
Money settles directly into your own external channel (bank account, Till,
or Paybill), straight from the payer. PayHero never holds the funds — it
only facilitates the transaction. You route with channel_id.
Kenya collections use the V1 endpoint POST /api/v2/payments. There are two
distinct kinds of collection — the difference is where the money settles:
Payment Channel collections
Money settles directly into your own external channel (bank account, Till,
or Paybill), straight from the payer. PayHero never holds the funds — it
only facilitates the transaction. You route with channel_id.
Wallet-based collections
Money settles into your PayHero-managed wallet (your customer wallet within
PayHero). You later withdraw it to your preferred channel — bank, phone,
Paybill, or Till. You route with network_code.
Both use the same endpoint; the fields you send decide the type.
Use these when you want funds to land directly in your external channel (bank,
Till, or Paybill). PayHero doesn’t custody the money — it moves from the payer
straight to your channel, and PayHero simply facilitates the collection. Route the
collection with a channel_id.
Each external channel you configure (bank, Till, Paybill, or wallet) has a numeric
id. Call GET /api/v2/payment_channels to list them, then use the id of an
active channel as the channel_id in your collection request.
curl "$PH_BASE_URL/api/v2/payment_channels" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD"<?php$ch = curl_init("$baseUrl/api/v2/payment_channels");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_USERPWD => "$username:$password",]);$response = curl_exec($ch);curl_close($ch);echo $response;import requests
response = requests.get( f"{base_url}/api/v2/payment_channels", auth=(username, password),)print(response.json())const auth = Buffer.from(`${username}:${password}`).toString("base64");
const response = await fetch(`${baseUrl}/api/v2/payment_channels`, { headers: { Authorization: `Basic ${auth}` },});
console.log(await response.json());The response lists your channels (newest activity first):
{ "payment_channels": [ { "id": 8949, "account_number": "1234567890", "channel_type": "bank", "is_active": true, "short_code": "123456", "description": "My Bank Account", "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" }, { "id": 9765, "account_number": "0123456789", "channel_type": "paybill", "is_active": true, "short_code": "654321", "description": "My Paybill", "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" }, { "id": 1501, "account_number": "0700111222", "channel_type": "wallet", "is_active": true, "short_code": "100200", "description": "My Wallet", "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:00:00Z" }, { "id": 1389, "account_number": "445566", "channel_type": "till", "is_active": false, "short_code": "445566", "description": "My Till", "created_at": "2026-01-10T08:00:00Z", "updated_at": "2026-01-10T08:00:00Z" } ], "pagination": { "count": 4, "page": 1, "per": 20, "num_pages": 1, "next_page": null, "prev_page": null }}| Field | Description |
|---|---|
id |
The channel ID — pass this as channel_id when creating a collection. |
account_number |
Destination account (for bank / paybill channels). |
channel_type |
bank, paybill, till, or wallet. |
is_active |
Only active channels can be used in a collection. |
short_code |
The Till / Paybill / bank paybill number. |
description |
Human-friendly label for the channel. |
created_at |
When the channel was created. |
updated_at |
When the channel was last updated. |
curl -X POST "$PH_BASE_URL/api/v2/payments" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{ "amount": 10, "phone_number": "0712345678", "provider": "m-pesa", "channel_id": 8949, "account_id": 63, "external_reference": "test_ext", "callback_url": "https://your-system.com/webhooks/payhero" }'<?php$ch = curl_init("$baseUrl/api/v2/payments");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => "$username:$password", CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode([ "amount" => 10, "phone_number" => "0712345678", "provider" => "m-pesa", "channel_id" => 8949, "account_id" => 63, "external_reference" => "test_ext", "callback_url" => "https://your-system.com/webhooks/payhero", ]),]);$response = curl_exec($ch);curl_close($ch);echo $response;import requests
response = requests.post( f"{base_url}/api/v2/payments", auth=(username, password), json={ "amount": 10, "phone_number": "0712345678", "provider": "m-pesa", "channel_id": 8949, "account_id": 63, "external_reference": "test_ext", "callback_url": "https://your-system.com/webhooks/payhero", },)print(response.json())const auth = Buffer.from(`${username}:${password}`).toString("base64");
const response = await fetch(`${baseUrl}/api/v2/payments`, { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 10, phone_number: "0712345678", provider: "m-pesa", channel_id: 8949, account_id: 63, external_reference: "test_ext", callback_url: "https://your-system.com/webhooks/payhero", }),});
console.log(await response.json());Use these when you want funds to land in your PayHero wallet. PayHero holds the
balance for you, and you later withdraw it to your preferred
channel (bank, phone, Paybill, or Till). Route the collection with a
network_code.
curl -X POST "$PH_BASE_URL/api/v2/payments" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{ "amount": 10, "phone_number": "0712345678", "provider": "sasapay", "network_code": "63902", "account_id": 63, "external_reference": "test_ext", "callback_url": "https://your-system.com/webhooks/payhero" }'<?php$ch = curl_init("$baseUrl/api/v2/payments");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => "$username:$password", CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode([ "amount" => 10, "phone_number" => "0712345678", "provider" => "sasapay", "network_code" => "63902", "account_id" => 63, "external_reference" => "test_ext", "callback_url" => "https://your-system.com/webhooks/payhero", ]),]);$response = curl_exec($ch);curl_close($ch);echo $response;import requests
response = requests.post( f"{base_url}/api/v2/payments", auth=(username, password), json={ "amount": 10, "phone_number": "0712345678", "provider": "sasapay", "network_code": "63902", "account_id": 63, "external_reference": "test_ext", "callback_url": "https://your-system.com/webhooks/payhero", },)print(response.json())const auth = Buffer.from(`${username}:${password}`).toString("base64");
const response = await fetch(`${baseUrl}/api/v2/payments`, { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 10, phone_number: "0712345678", provider: "sasapay", network_code: "63902", account_id: 63, external_reference: "test_ext", callback_url: "https://your-system.com/webhooks/payhero", }),});
console.log(await response.json());For an offline wallet collection, add is_offline: true (and a merchant_fee).
No STK push is sent — the customer completes payment manually via M-Pesa
Paybill, using the CheckoutRequestID from the response as the account
reference.
is_offline: true and read the CheckoutRequestID from
the response (e.g. 29119).756756553125#<CheckoutRequestID> — e.g. 553125#29119A successful request returns 201 with a CheckoutRequestID:
{ "success": true, "status": "QUEUED", "reference": "UFD003217286.iI", "CheckoutRequestID": "29119", "external_reference": "test_ext"}| Type | Where money settles | Key field |
|---|---|---|
| Payment Channel | Your external bank / Till / Paybill | channel_id |
| Wallet-based | Your PayHero wallet (withdraw later) | network_code |