Appearance
Collect with M-Pesa in Kenya
Send an M-Pesa prompt to your customer's phone. They enter their M-Pesa PIN, and the money lands in your Payd account.
POST
https://api.payd.money/api/v2/payments/payinHow it works
- You send the amount and the customer's phone number.
- Payd answers with a reference, and the customer's phone shows an M-Pesa prompt.
- The customer enters their PIN, or cancels. Prompts expire after about a minute.
- Payd posts the result to your
callback_url, and the status endpoint showssuccessorfailed.
Request
bash
curl https://api.payd.money/api/v2/payments/payin \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{
"amount": 1500,
"currency": "KES",
"country": "KE",
"transaction_channel": "mobile",
"customer": {
"first_name": "Amina",
"last_name": "Wanjiru",
"phone_number": "254712345678",
"email": "amina@example.com"
},
"vendor_config": { "account_id": "4f28836c-2f1e-4d9b-9a51-6f3c2b1d8e70" },
"provider_config": {},
"payment_config": {
"account_number": "254712345678",
"remark": "Order 8842",
"reference": "order-8842",
"callback_url": "https://example.com/webhooks/payd"
}
}'js
const auth = Buffer.from(
`${process.env.PAYD_API_USERNAME}:${process.env.PAYD_API_PASSWORD}`,
).toString('base64')
const res = await fetch('https://api.payd.money/api/v2/payments/payin', {
method: 'POST',
headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 1500,
currency: 'KES',
country: 'KE',
transaction_channel: 'mobile',
customer: { first_name: 'Amina', last_name: 'Wanjiru', phone_number: '254712345678' },
vendor_config: { account_id: process.env.PAYD_ACCOUNT_ID },
provider_config: {},
payment_config: {
account_number: '254712345678',
remark: 'Order 8842',
reference: 'order-8842',
callback_url: 'https://example.com/webhooks/payd',
},
}),
signal: AbortSignal.timeout(35_000),
})
const body = await res.json()
if (!res.ok) throw new Error(`${body.error_code}: ${body.error_message}`)
const reference = body.gateway_response.merchant_reference // store this with your orderpython
import os
import requests
res = requests.post(
"https://api.payd.money/api/v2/payments/payin",
auth=(os.environ["PAYD_API_USERNAME"], os.environ["PAYD_API_PASSWORD"]),
json={
"amount": 1500,
"currency": "KES",
"country": "KE",
"transaction_channel": "mobile",
"customer": {"first_name": "Amina", "last_name": "Wanjiru", "phone_number": "254712345678"},
"vendor_config": {"account_id": os.environ["PAYD_ACCOUNT_ID"]},
"provider_config": {},
"payment_config": {
"account_number": "254712345678",
"remark": "Order 8842",
"reference": "order-8842",
"callback_url": "https://example.com/webhooks/payd",
},
},
timeout=35,
)
body = res.json()
res.raise_for_status()
reference = body["gateway_response"]["merchant_reference"] # store this with your orderphp
<?php
$payload = [
'amount' => 1500,
'currency' => 'KES',
'country' => 'KE',
'transaction_channel' => 'mobile',
'customer' => ['first_name' => 'Amina', 'last_name' => 'Wanjiru', 'phone_number' => '254712345678'],
'vendor_config' => ['account_id' => getenv('PAYD_ACCOUNT_ID')],
'provider_config' => new stdClass(),
'payment_config' => [
'account_number' => '254712345678',
'remark' => 'Order 8842',
'reference' => 'order-8842',
'callback_url' => 'https://example.com/webhooks/payd',
],
];
$ch = curl_init('https://api.payd.money/api/v2/payments/payin');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => getenv('PAYD_API_USERNAME') . ':' . getenv('PAYD_API_PASSWORD'),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_TIMEOUT => 35,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$reference = $body['gateway_response']['merchant_reference'] ?? null; // store this with your orderFields
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in KES, in whole shillings. Today the range is 1 to 250,000. The API is the source of truth: see limits. |
currency | string | Yes | KES. |
country | string | No | KE. |
transaction_channel | string | Yes | mobile. |
customer.first_name | string | Yes | Customer's first name. |
customer.last_name | string | Yes | Customer's last name. |
customer.phone_number | string | Yes | M-Pesa number in the form 254712345678. Payd also accepts 0712345678 and +254712345678. |
customer.email | string | No | Customer's email. |
vendor_config.account_id | string | Yes | Your account id. See authentication. |
provider_config | object | Yes | Send {}. |
payment_config.account_number | string | No | The number to prompt, if it is not customer.phone_number. |
payment_config.remark | string | No | A short note kept with the payment. |
payment_config.reference | string | No | Your own id for the payment. Payd returns it as external_reference in webhooks. |
payment_config.callback_url | string | No | Where Payd posts the result. See webhooks. |
wallet_type | string | No | fiat (default) or crypto. See wallets. |
Response
json
{
"gateway_response": {
"success": true,
"merchant_reference": "9BD103350408eR",
"checkout_request_id": "7c1e9a52d4b8"
}
}| Field | Meaning |
|---|---|
success | true when the prompt was sent. It does not mean the customer has paid. |
merchant_reference | Payd's reference for the payment. Store it, and use it to check the status. |
checkout_request_id | The network's id for the prompt. Store it to help support find the payment. |
message | Text about the request. Do not parse it. |
If success is false, the prompt was not sent. Show message in your logs and let the customer try again.
Confirm the payment
Wait for the webhook, then confirm with the status endpoint:
bash
curl https://api.payd.money/api/v2/payments/9BD103350408eR/status \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD"If the customer ignores the prompt, the payment ends as failed. Ask them to try again, which sends a new prompt with a new reference.
Tips for a good checkout
- Tell the customer to keep their phone unlocked and watch for the prompt.
- Show a "Waiting for your M-Pesa PIN" screen with a button to send the prompt again. Send a new prompt only after the first one has
failed. - If the customer does not have a name on file with you, ask for one: both names are required.
The older collection endpoint
Integrations built before October 2026 use POST /api/v2/payments. It still works, and new integrations should use the endpoint above.
POST
https://api.payd.money/api/v2/paymentsbash
curl https://api.payd.money/api/v2/payments \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{
"channel": "MPESA",
"amount": 1500,
"phone_number": "0712345678",
"narration": "Order 8842",
"currency": "KES",
"callback_url": "https://example.com/webhooks/payd"
}'| Field | Required | Description |
|---|---|---|
channel | Yes | MPESA. |
amount | Yes | Amount in KES, from 10 to 250,000. |
phone_number | Yes | Kenyan number: 0712345678, 254712345678 or +254712345678. |
narration | Yes | A short note about the payment. |
currency | Yes | KES. |
callback_url | Yes | Where Payd posts the result. |
username | No | Older integrations send their Payd username here. Payd ignores it and uses the account of your API key. |
json
{
"payment_method": "mobile",
"status": "SUCCESS",
"transaction_reference": "9BD103350408eR",
"success": true,
"message": "Processed successfully",
"trackingId": "",
"reference": "",
"result": null
}"status": "SUCCESS" here means the prompt was sent, not that the customer paid. Errors on this endpoint use the older error body, and webhooks can come in either format.

