Overview/api/v1/payments · JSON · POST
Every integration uses four endpoints — one to start each money movement and one to poll its state. Final outcomes are also pushed to your callback_url, so you rarely need to poll.
Make payment
Charge a customer's mobile wallet — a collection / debit.
Payment status
Look up the current state of a payment by reference.
Make withdrawal
Disburse from your PalmKash balance to a wallet.
Withdrawal status
Look up the current state of a withdrawal by reference.
Base URL & conventions
| Item | Value |
|---|---|
| Production | https://dashboard.palmkash.com/api/v1 |
| Currency | RWF — amounts are whole Rwandan francs |
| Operators | MTN 78 / 79 Airtel 72 / 73 |
| Phone format | Auto-detected & normalised to 250 + last 9 digits |
Authenticationthree checks, enforced before processing
Each request must satisfy all three checks below. Missing any one is rejected before your transaction is created.
| Requirement | Where | Purpose |
|---|---|---|
| API token | Authorization: Bearer <token>or api_token in body | Identifies & authorises your merchant. Issued in the dashboard. |
merchant_id | Request body | Your merchant key. Must match the token owner. |
| HMAC signature | X-Merchant-Key · X-Timestamp · X-Signature | Proves the body wasn't tampered with and isn't replayed. |
Authorization: Bearer <YOUR_API_TOKEN>
Content-Type: application/json
X-Merchant-Key: <YOUR_MERCHANT_KEY>
X-Timestamp: 1699999999
X-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
merchant_id, the X-Merchant-Key header, and the HMAC secret are all the same value — your merchant key, e.g. PK_34556ardeaaa.Signing a requestHMAC-SHA256 over timestamp + canonical body
The signature is an HMAC-SHA256 over the timestamp and the exact JSON body, keyed with your merchant key. Get the body encoding right — the server re-encodes with unescaped slashes and unicode before comparing.
json_encode(json_decode(body), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)."{timestamp}.{canonical_body}": timestamp, a literal dot, then the JSON.hash_hmac('sha256', payload, merchant_key), hex-encoded. Send as X-Signature.$merchantKey = 'PK_34556ardeaaa';
$timestamp = (string) time();
$payloadArr = [
'merchant_id' => $merchantKey,
'phone_number' => '250789777777',
'amount' => 10000,
'currency' => 'RWF',
'client_reference' => 'INV-123456',
'callback_url' => 'https://shop.com/callback',
];
// Canonical JSON — must match the server's re-encoding
$body = json_encode($payloadArr, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$signature = hash_hmac('sha256', $timestamp . '.' . $body, $merchantKey);
// X-Merchant-Key: $merchantKey
// X-Timestamp: $timestamp
// X-Signature: $signature (send $body as the raw request body)
const crypto = require('crypto');
const merchantKey = 'PK_34556ardeaaa';
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = {
merchant_id: merchantKey,
phone_number: '250789777777',
amount: 10000,
currency: 'RWF',
client_reference: 'INV-123456',
callback_url: 'https://shop.com/callback',
};
const body = JSON.stringify(payload); // no escaped slashes by default
const signature = crypto
.createHmac('sha256', merchantKey)
.update(`${timestamp}.${body}`)
.digest('hex');
// Send `body` as the raw request body with the three X- headers.
401 Invalid signature.Request modelaccept now, resolve later
Money movement is asynchronous. An accepted request returns pending with a reference; the final completed / failed state arrives via callback (or by polling the status endpoint).
reference (a UUID) and an initial status.completed or failed.callback_url. Store the reference to match it.reference returned to you is the transaction's UUID. Use it — or your own client_reference — when polling status and reconciling callbacks.Make paymentcollect from a customer wallet
Charge a customer's mobile money wallet. The customer approves the debit with their MoMo / Airtel PIN.
Body parameters
| Field | Type | Description | |
|---|---|---|---|
merchant_id | string | req | Your merchant key. |
phone_number | string | req | Customer MSISDN. Rwandan MTN/Airtel; last 9 digits used. |
amount | number | req | Amount in RWF. Minimum 1. |
callback_url | url | req | HTTPS URL PalmKash POSTs the final result to. |
client_reference | string | req | Your own reference (used for status lookups). |
currency | string | opt | Defaults to RWF. |
customer_name | string | opt | Required if customer_email is set. Creates a client record. |
customer_email | opt | Optional customer email. |
{
"merchant_id": "PK_34556ardeaaa",
"phone_number": "250789777777",
"amount": 10000,
"currency": "RWF",
"client_reference": "INV-123456",
"callback_url": "https://shop.com/callback",
"customer_name": "John Brown",
"customer_email": "john@example.com"
}
{
"status": "pending",
"reference": "8917c345-4791-4285-a416-62f24b6982db"
}
"status": "failed" in this same response. Otherwise the payment stays pending until the customer approves — watch for the callback.Payment statuspoll a payment by reference
Look up a payment by your client_reference or the reference (UUID) returned at initiation.
| Field | Type | Description | |
|---|---|---|---|
merchant_id | string | req | Your merchant key. |
reference | string | req | Your client_reference, or the returned UUID. |
{
"merchant_id": "PK_34556ardeaaa",
"reference": "INV-123456"
}
{
"status": "completed",
"message": "12356789",
"reference": "8917c345-4791-4285-a416-62f24b6982db"
}
{
"status": "failed",
"message": "resource not found"
}
Make withdrawaldisburse to a customer wallet
Disburse funds from your PalmKash balance to a customer's mobile wallet. Available balance — completed payments minus withdrawals — is checked before the request is accepted.
Body parameters
| Field | Type | Description | |
|---|---|---|---|
merchant_id | string | req | Your merchant key. |
phone_number | string | req | Recipient MSISDN (Rwandan MTN/Airtel). |
amount | number | req | Amount in RWF. Minimum 100. Fees are deducted from it. |
callback_url | url | req | HTTPS URL PalmKash POSTs the final result to. |
client_reference | string | req | Your own reference for this withdrawal. |
customer_name | string | opt | Required if customer_email is set. |
customer_email | opt | Optional recipient email. |
{
"merchant_id": "PK_34556ardeaaa",
"phone_number": "250789777777",
"amount": 10000,
"currency": "RWF",
"client_reference": "PAYOUT-987",
"callback_url": "https://shop.com/callback"
}
{
"status": "pending",
"reference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639"
}
{
"status": "error",
"message": "insufficient balance"
}
Withdrawal statuspoll a withdrawal by reference
Look up a withdrawal by your client_reference or the returned UUID reference.
| Field | Type | Description | |
|---|---|---|---|
merchant_id | string | req | Your merchant key. |
reference | string | req | Your client_reference, or the returned UUID. |
{
"status": "completed",
"message": "200",
"reference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639"
}
CallbacksPalmKash → your server, on resolution
When a transaction leaves the pending state, PalmKash sends an HTTP POST to the callback_url you supplied. Respond 200 to acknowledge.
Payment callback
{
"status": "completed",
"message": "12356789",
"reference": "8917c345-4791-4285-a416-62f24b6982db",
"client_reference": "INV-123456"
}
Withdrawal callback
{
"status": "completed",
"message": "200",
"reference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639"
}
referenceis the transaction UUID (same value returned at initiation);messagecarries the gateway reference / status code.- Match on
reference(andclient_referencefor payments), then confirm via the status endpoint before releasing goods. - Treat callbacks as idempotent — the same final status may be delivered more than once.
Statusesthe transaction lifecycle
| Status | Final? | Meaning |
|---|---|---|
| pending | no | Accepted and awaiting the customer / operator. |
| completed | yes | Money moved successfully. |
| failed | yes | Did not go through. message holds the reason/code. |
Errorsstandard HTTP codes, checked pre-processing
Validation and signature failures are returned before the transaction is created.
| Code | When | Body |
|---|---|---|
| 422 | Validation failed, missing signature headers, or unknown merchant | { "success": false, "error": "…", "errors": {…} } |
| 401 | Bad API token, invalid signature, or expired timestamp (>300s) | { "message": "Invalid signature" } |
| 400 | Unauthorised merchant, or insufficient balance (withdrawals) | { "status": "error", "message": "insufficient balance" } |
| 500 | Upstream / system error — retry later | { "status": "error", "message": "system error try again later" } |
{
"success": false,
"error": "invalid data",
"errors": {
"phone_number": ["The phone_number field is required."]
}
}
Signature & auth messages
| Message | Code | Cause |
|---|---|---|
Missing signature headers | 422 | A required X- header is absent. |
Request expired | 401 | X-Timestamp is >300s from server time. |
Invalid merchant | 422 | X-Merchant-Key matches no merchant. |
Invalid signature | 401 | Computed HMAC ≠ X-Signature (usually a body-encoding mismatch). |
Unauthorized | 401 | API token missing or not valid for this merchant. |