PalmKash Merchant API Endpoints Auth

PalmKash · Merchant API · v1

Move money in Rwanda, in one request.

Collect payments from and disburse to MTN MoMo and Airtel Money wallets over a signed, asynchronous REST API. Four endpoints, one callback, RWF settled.

4
Endpoints
RWF
Currency
2
Operators
HMAC
SHA-256 signed
Payment lifecycle
① request → debit ② callback ← result MERCH your server PALM KASH CUST MoMo · Airtel
§ 01

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.

POST

Make payment

Charge a customer's mobile wallet — a collection / debit.

POST

Payment status

Look up the current state of a payment by reference.

POST

Make withdrawal

Disburse from your PalmKash balance to a wallet.

POST

Withdrawal status

Look up the current state of a withdrawal by reference.

Base URL & conventions

ItemValue
Productionhttps://dashboard.palmkash.com/api/v1
CurrencyRWF — amounts are whole Rwandan francs
OperatorsMTN 78 / 79   Airtel 72 / 73
Phone formatAuto-detected & normalised to 250 + last 9 digits
§ 02

Authenticationthree checks, enforced before processing

Each request must satisfy all three checks below. Missing any one is rejected before your transaction is created.

RequirementWherePurpose
API tokenAuthorization: Bearer <token>
or api_token in body
Identifies & authorises your merchant. Issued in the dashboard.
merchant_idRequest bodyYour merchant key. Must match the token owner.
HMAC signatureX-Merchant-Key · X-Timestamp · X-SignatureProves the body wasn't tampered with and isn't replayed.
Required headers
Authorization:  Bearer <YOUR_API_TOKEN>
Content-Type:   application/json
X-Merchant-Key: <YOUR_MERCHANT_KEY>
X-Timestamp:    1699999999
X-Signature:    9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
i
Your merchant_id, the X-Merchant-Key header, and the HMAC secret are all the same value — your merchant key, e.g. PK_34556ardeaaa.
§ 03

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.

Take the timestamp — current Unix time in seconds. Must be within 300 seconds of PalmKash's clock, or the request is rejected as expired.
Canonicalise the body — json_encode(json_decode(body), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).
Build the payload — "{timestamp}.{canonical_body}": timestamp, a literal dot, then the JSON.
Sign it — hash_hmac('sha256', payload, merchant_key), hex-encoded. Send as X-Signature.
PHPcompute the 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)
Node.jscompute the signature
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.
!
Sign the exact bytes you send. If the body is re-serialised differently (key order, spacing, escaping) after signing, the signatures won't match and you'll get 401 Invalid signature.
§ 04

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).

Initiate — POST the payment or withdrawal. You receive a reference (a UUID) and an initial status.
Customer acts — the customer approves the MoMo / Airtel prompt on their handset (for payments).
Resolve — PalmKash settles with the operator; the transaction becomes completed or failed.
Notify — PalmKash POSTs the outcome to your callback_url. Store the reference to match it.
i
The reference returned to you is the transaction's UUID. Use it — or your own client_reference — when polling status and reconciling callbacks.
§ 05

Make paymentcollect from a customer wallet

Charge a customer's mobile money wallet. The customer approves the debit with their MoMo / Airtel PIN.

POST/api/v1/payments/make-paymentcollect

Body parameters

FieldTypeDescription
merchant_idstringreqYour merchant key.
phone_numberstringreqCustomer MSISDN. Rwandan MTN/Airtel; last 9 digits used.
amountnumberreqAmount in RWF. Minimum 1.
callback_urlurlreqHTTPS URL PalmKash POSTs the final result to.
client_referencestringreqYour own reference (used for status lookups).
currencystringoptDefaults to RWF.
customer_namestringoptRequired if customer_email is set. Creates a client record.
customer_emailemailoptOptional customer email.
POST/payments/make-payment
{
  "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"
}
200accepted
{
  "status": "pending",
  "reference": "8917c345-4791-4285-a416-62f24b6982db"
}
i
An immediate provider rejection returns "status": "failed" in this same response. Otherwise the payment stays pending until the customer approves — watch for the callback.
§ 06

Payment statuspoll a payment by reference

Look up a payment by your client_reference or the reference (UUID) returned at initiation.

POST/api/v1/payments/get-payment-statuspoll
FieldTypeDescription
merchant_idstringreqYour merchant key.
referencestringreqYour client_reference, or the returned UUID.
POSTrequest
{
  "merchant_id": "PK_34556ardeaaa",
  "reference": "INV-123456"
}
200found
{
  "status": "completed",
  "message": "12356789",
  "reference": "8917c345-4791-4285-a416-62f24b6982db"
}
404not found
{
  "status": "failed",
  "message": "resource not found"
}
§ 07

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.

POST/api/v1/payments/make-withdrawdisburse

Body parameters

FieldTypeDescription
merchant_idstringreqYour merchant key.
phone_numberstringreqRecipient MSISDN (Rwandan MTN/Airtel).
amountnumberreqAmount in RWF. Minimum 100. Fees are deducted from it.
callback_urlurlreqHTTPS URL PalmKash POSTs the final result to.
client_referencestringreqYour own reference for this withdrawal.
customer_namestringoptRequired if customer_email is set.
customer_emailemailoptOptional recipient email.
POST/payments/make-withdraw
{
  "merchant_id": "PK_34556ardeaaa",
  "phone_number": "250789777777",
  "amount": 10000,
  "currency": "RWF",
  "client_reference": "PAYOUT-987",
  "callback_url": "https://shop.com/callback"
}
200accepted
{
  "status": "pending",
  "reference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639"
}
400insufficient balance
{
  "status": "error",
  "message": "insufficient balance"
}
§ 08

Withdrawal statuspoll a withdrawal by reference

Look up a withdrawal by your client_reference or the returned UUID reference.

POST/api/v1/payments/get-withdraw-statuspoll
FieldTypeDescription
merchant_idstringreqYour merchant key.
referencestringreqYour client_reference, or the returned UUID.
200found
{
  "status": "completed",
  "message": "200",
  "reference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639"
}
§ 09

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

POST{your-callback-url}payment
BODYpayment result
{
  "status": "completed",
  "message": "12356789",
  "reference": "8917c345-4791-4285-a416-62f24b6982db",
  "client_reference": "INV-123456"
}

Withdrawal callback

POST{your-callback-url}withdrawal
BODYwithdrawal result
{
  "status": "completed",
  "message": "200",
  "reference": "f4401bd2-1568-4140-bf2d-eb77d2b2b639"
}
  • reference is the transaction UUID (same value returned at initiation); message carries the gateway reference / status code.
  • Match on reference (and client_reference for payments), then confirm via the status endpoint before releasing goods.
  • Treat callbacks as idempotent — the same final status may be delivered more than once.
§ 10

Statusesthe transaction lifecycle

StatusFinal?Meaning
pendingnoAccepted and awaiting the customer / operator.
completedyesMoney moved successfully.
failedyesDid not go through. message holds the reason/code.
§ 11

Errorsstandard HTTP codes, checked pre-processing

Validation and signature failures are returned before the transaction is created.

CodeWhenBody
422Validation failed, missing signature headers, or unknown merchant{ "success": false, "error": "…", "errors": {…} }
401Bad API token, invalid signature, or expired timestamp (>300s){ "message": "Invalid signature" }
400Unauthorised merchant, or insufficient balance (withdrawals){ "status": "error", "message": "insufficient balance" }
500Upstream / system error — retry later{ "status": "error", "message": "system error try again later" }
422validation error
{
  "success": false,
  "error": "invalid data",
  "errors": {
    "phone_number": ["The phone_number field is required."]
  }
}

Signature & auth messages

MessageCodeCause
Missing signature headers422A required X- header is absent.
Request expired401X-Timestamp is >300s from server time.
Invalid merchant422X-Merchant-Key matches no merchant.
Invalid signature401Computed HMAC ≠ X-Signature (usually a body-encoding mismatch).
Unauthorized401API token missing or not valid for this merchant.