PP
PalmPesa v2
Developer Documentation · API v2

PalmPesa Payments API v2

Collect mobile-money payments in Tanzania with one API call. The customer approves a prompt on their phone, and PalmPesa notifies your server when the payment is final. Built and maintained by Eagle Star Technology.

v2API Version
TZSCurrency
JSONOver HTTPS
USSDPush to Phone

How a payment works

  • 1
    Your server requests a payment

    Send the amount, the customer's phone number and your callback_url. PalmPesa answers 202 Accepted with an order_id straight away and sends the prompt in the background.

  • 2
    The customer approves on their phone

    The customer receives a mobile-money prompt (USSD push) and enters their PIN. Supported wallets include M-Pesa, Mixx by Yas (Tigo Pesa), Airtel Money and HaloPesa.

  • 3
    PalmPesa notifies your server

    When the payment reaches a final state, PalmPesa sends a callback to your callback_url. You can also check the status at any time. Only COMPLETED means the money was received.

Getting Started

  • 1
    Create a developer account

    Register at palmpesa.co.tz and complete the developer registration and verification.

  • 2
    Get your API token

    After your account is approved, generate your API token from the dashboard. Requests made before approval return 403.

  • 3
    Prepare a callback endpoint

    Expose a public HTTPS URL on your server that accepts POST requests and answers 200 OK quickly. See Callbacks.

  • 4
    Test with a small amount

    Send a real payment of a few hundred TZS to your own phone and confirm your callback handler records it before going live.

Keep your token on the server

Never put your API token in mobile apps, browser JavaScript or public repositories. Call the API only from your server.

Try it in Postman

PalmPesa API v2 Postman collection

Request Payment and Payment Status, ready to send, with example responses for each.

Download collection JSON · Postman collection format v2.1
  • 1
    Import

    In Postman, choose Import and select the downloaded file.

  • 2
    Set api_token and your test details

    Open the collection's Variables tab and enter your api_token, your own phone number as payer_phone (for example 255712345678) and your HTTPS callback_url.

  • 3
    Send "Request payment"

    Approve the prompt on your phone, then send Get payment status. The order_id is saved for you, and every send of Request payment uses a new transaction_id.

These are real payments: each one charges amount TZS (1,000 by default) to payer_phone. Don't share or export the collection after adding your token.

Authentication

Send your API token as a Bearer token in the Authorization header of every request. Always send Accept: application/json so that errors come back as JSON.

HTTP Headers
Authorization: Bearer YOUR_API_TOKEN
Content-Type:  application/json
Accept:        application/json
Status Body Meaning
401 {"error":"Unauthorized"} No Authorization: Bearer header was sent.
401 {"error":"Invalid Token"} The token is wrong or has been replaced.
403 {"error":"Account verification pending. API access is available after admin approval."} Your account has not been approved yet.

Base URL

BASE https://palmpesa.co.tz

Prepend the base URL to every path on this page, for example https://palmpesa.co.tz/api/v2/async/payments.

Request Payment

Endpoint 01

Starts a mobile-money payment. PalmPesa records the request and answers 202 Accepted straight away, then sends the prompt to the customer's phone in the background. Follow the payment with callbacks or the Payment Status endpoint.

POST https://palmpesa.co.tz/api/v2/async/payments

Request Headers

Header Required Value
Authorization Required Bearer YOUR_API_TOKEN
Content-Type Required application/json
Accept Required application/json

Request Body

Parameter Type Required Description Example
transaction_id string Required Your own unique ID for this payment. Up to 191 characters: letters, digits, . _ : -. Reusing it safely retries the same payment (see Retries). "ORDER-2026-000123"
amount integer Required Amount in whole Tanzanian shillings, 100 to 100,000,000. 10000
currency string Required Must be TZS. "TZS"
phone string Required The customer's mobile-money number. 0712345678, 712345678, +255 712 345 678 and 255712345678 are all accepted and normalised to 2556XXXXXXXX or 2557XXXXXXXX. "255712345678"
name string Required Customer name, up to 255 characters. "Juma Hassan"
email string Required A valid email address for the customer. "juma@example.com"
callback_url string Required Where PalmPesa sends the result. Must be a public HTTPS URL (no localhost or private IP addresses), up to 2048 characters. "https://shop.example.com/palmpesa/callback"
buyer_remarks string Optional Note shown with the payment, up to 255 characters. "Payment for order 123"
merchant_remarks string Optional Your internal note, up to 255 characters. "Web shop"

Example Request

cURL
curl -X POST 'https://palmpesa.co.tz/api/v2/async/payments' \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "transaction_id": "ORDER-2026-000123",
    "amount": 10000,
    "currency": "TZS",
    "phone": "255712345678",
    "name": "Juma Hassan",
    "email": "juma@example.com",
    "callback_url": "https://shop.example.com/palmpesa/callback",
    "buyer_remarks": "Payment for order 123"
  }'

Response 202 Accepted

JSON
{
  "success": true,
  "message": "Payment accepted.",
  "data": {
    "transaction_id": "ORDER-2026-000123",
    "order_id":       "PALMPESAV2A20261008120000123EFGH5678",
    "status":         "INITIATING",
    "failure_reason": null,
    "amount":         10000,
    "currency":       "TZS",
    "reference":      null,
    "channel":        null,
    "created_at":     "2026-10-08 12:00:00",
    "updated_at":     "2026-10-08 12:00:00",
    "status_url":     "https://palmpesa.co.tz/api/v2/async/payments/PALMPESAV2A20261008120000123EFGH5678"
  }
}

Store transaction_id and order_id with your order. A 202 and INITIATING do not mean the money was received: wait for a payment.completed callback.

Response fields

Field Description
transaction_id Your ID from the request.
order_id PalmPesa's ID for the payment. Use it with the Payment Status endpoint.
status The current status. Always INITIATING on a new payment.
failure_reason Why the prompt could not be sent, when the status is INITIATION_FAILED. Otherwise null.
amount, currency As requested.
reference The payment gateway's reference for the payment, once it has one. null at first.
channel The mobile-money network that handled the payment, for example TIGOPESATZ. null until the network reports it.
created_at, updated_at When the payment was created and when it last changed.
status_url The full Payment Status URL for this payment.

If the prompt cannot be sent

The request itself still succeeds with 202. If the mobile-money provider refuses the payment or is unavailable, the payment becomes INITIATION_FAILED with a failure_reason, and you receive a payment.initiation_failed callback. Nothing was charged. To try again, send a new request with a new transaction_id.

Retries and duplicate requests

Your account plus transaction_id identify a payment. If your request times out, send the same request again:

  • Same transaction_id and same details: 202 Accepted with the original order_id and the payment's current status. The customer is not prompted a second time.
  • Same transaction_id with different details: 409 Conflict {"message":"This transaction_id was already used with different payment details."}
  • A transaction_id belongs to one payment for good. To charge the customer again after INITIATION_FAILED, CANCELLED, REJECTED or EXPIRED, use a new transaction_id.

Payment Status

Endpoint 02

Returns the current status of a payment created with Request Payment. Use the status_url from that response, or put the order_id in the path.

GET https://palmpesa.co.tz/api/v2/async/payments/{order_id}

Example Request

cURL
curl 'https://palmpesa.co.tz/api/v2/async/payments/PALMPESAV2A20261008120000123EFGH5678' \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Accept: application/json'

Response 200 OK

JSON
{
  "success": true,
  "data": {
    "transaction_id": "ORDER-2026-000123",
    "order_id":       "PALMPESAV2A20261008120000123EFGH5678",
    "status":         "COMPLETED",
    "failure_reason": null,
    "amount":         10000,
    "currency":       "TZS",
    "reference":      "0289999288",
    "channel":        "TIGOPESATZ",
    "created_at":     "2026-10-08 12:00:00",
    "updated_at":     "2026-10-08 12:01:30",
    "status_url":     "https://palmpesa.co.tz/api/v2/async/payments/PALMPESAV2A20261008120000123EFGH5678"
  }
}

The fields are the same as in the Request Payment response.

Response 404 Not Found

The order does not exist or belongs to another account.

JSON
{ "message": "Payment not found.", "order_id": "PALMPESAV2A20261008120000123EFGH5678" }
Polling

This endpoint reads PalmPesa's records and does not contact the mobile-money network, so it is cheap to call. Prefer callbacks. If you poll, wait a few seconds between calls (limit: 120 requests per minute) and stop once the status is final.

Payment Statuses

Status Meaning Final
INITIATING Accepted; the prompt has not been sent yet. No
PENDING Prompt sent; waiting for the customer to approve. No
AMBIGUOUS The network did not confirm the prompt in time; PalmPesa is checking the outcome. Do not resend the payment. No
COMPLETED Money received. The only status that means paid. Yes
CANCELLED The customer cancelled or did not approve. Yes
REJECTED The mobile-money network rejected the payment. Yes
INITIATION_FAILED The prompt could not be sent. Nothing was charged. Retry with a new transaction_id. Yes
EXPIRED No payment was received: the customer had not paid within about an hour of the request. Do not fulfil the order. See Expired payments. Yes (a late COMPLETED can still follow)

Failure reasons

When a payment is INITIATION_FAILED, failure_reason explains why:

failure_reason Meaning
SELCOM_REJECTED_ORDER The payment gateway refused to create the order.
SELCOM_REJECTED_PUSH The gateway refused to send the prompt to the phone.
PROVIDER_UNAVAILABLE The gateway is temporarily unavailable; try again later.
PROVIDER_ERROR The gateway kept failing after several attempts.

Expired payments

If the customer hasn't paid within about an hour, the payment becomes EXPIRED and you receive a payment.expired callback. A mobile-money payment request stays open for 30 minutes, and PalmPesa waits a further grace period for late confirmations. Then, if the payment is still INITIATING, PENDING or AMBIGUOUS, PalmPesa checks it with the network one last time:

  • Paid: it becomes COMPLETED and you receive payment.completed.
  • Cancelled or rejected: it becomes CANCELLED or REJECTED, with the matching callback.
  • Anything else: it becomes EXPIRED and you receive a payment.expired callback.

This usually happens 45 to 60 minutes after the payment was created, occasionally later if the network cannot be reached. EXPIRED means no payment was received: do not fulfil the order. To collect the money, create a new payment with a new transaction_id.

Late results

A payment can still complete minutes after the prompt was sent, and in rare cases the network confirms it only after it has expired. You then receive payment.completed after payment.expired, and the status changes to COMPLETED. COMPLETED always means paid, whenever it arrives: fulfil the order, or contact the customer if you have already closed it. Keep accepting callbacks, deduplicate them on event_id, and never mark an order as paid until you receive COMPLETED.

Callbacks

When a payment reaches a final status, PalmPesa sends a POST request with a JSON body to the callback_url you supplied. There are no callbacks for INITIATING, PENDING or AMBIGUOUS.

Request headers sent to you

Header Always Value
Content-Type Yes application/json
X-PalmPesa-Event-Id Yes Same value as event_id in the body
X-PalmPesa-Timestamp Only if signing is enabled Unix time in seconds when the callback was signed
X-PalmPesa-Signature Only if signing is enabled sha256= followed by a hex HMAC (see signed callbacks)

Body

JSON — payment.completed
{
  "event":            "payment.completed",
  "event_id":         "82c12c18a92fc6f3bfb61f89bbf0b8f10f7790a3cfe7b0940734eb43eca87eb1",
  "transaction_id":   "ORDER-2026-000123",
  "order_id":         "PALMPESAV2A20261008120000123EFGH5678",
  "selcom_reference": "0289999288",
  "status":           "COMPLETED",
  "amount":           10000,
  "currency":         "TZS",
  "channel":          "TIGOPESATZ",
  "phone":            "255712345678",
  "processed_at":     "2026-10-08T12:01:31+03:00"
}

Events

event status What to do
payment.completed COMPLETED Money received. Fulfil the order.
payment.cancelled CANCELLED The customer cancelled. Do not fulfil.
payment.rejected REJECTED The network rejected the payment. Do not fulfil.
payment.initiation_failed INITIATION_FAILED The prompt was never sent and nothing was charged. Retry with a new transaction_id if you want to.
payment.expired EXPIRED Not paid within about an hour. Do not fulfil. A late payment.completed can still follow (see Expired payments).

Every event has the same fields. channel and selcom_reference may be null for payments that did not complete.

Responding and retries

  • Answer with any 2xx status within 10 seconds, for example 200 {"received": true}. Do slow work after you respond.
  • If your endpoint is unreachable or answers with a non-2xx status, PalmPesa retries delivery (up to 5 attempts in total).
  • You can receive the same event more than once. Use event_id as an idempotency key and process each event only once.
  • A payment.completed event is never followed by a different final status for the same payment. The reverse can happen: in rare cases payment.completed arrives after payment.expired. Treat it as paid.

Verifying a callback

Before you fulfil an order, check that the callback matches your records:

  • transaction_id and order_id belong to an order you created.
  • amount and currency equal what you requested.
  • You can also confirm with the Payment Status endpoint before fulfilling.

Signed callbacks (optional)

Signing is off unless PalmPesa has issued you a signing secret. Without one, callbacks carry no signature headers; rely on the checks above. With one, every callback also carries X-PalmPesa-Timestamp and X-PalmPesa-Signature, computed over the exact raw request body:

Signature
signed_data = X-PalmPesa-Timestamp + "." + raw_request_body
signature   = "sha256=" + hex( HMAC-SHA256(signed_data, YOUR_SIGNING_SECRET) )

Example callback handler (PHP)

PHP
<?php
$rawBody = file_get_contents('php://input');
$secret  = getenv('PALMPESA_SIGNING_SECRET'); // only if one was issued to you

if ($secret) {
    $timestamp = $_SERVER['HTTP_X_PALMPESA_TIMESTAMP'] ?? '';
    $received  = $_SERVER['HTTP_X_PALMPESA_SIGNATURE'] ?? '';
    $expected  = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

    if ($timestamp === '' || abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $received)) {
        http_response_code(401);
        exit;
    }
}

$event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);

// 1. Ignore events you have already processed (look up $event['event_id']).
// 2. Match transaction_id, order_id, amount and currency with your order.
// 3. Mark the order paid only when $event['status'] === 'COMPLETED',
//    even if it arrives after a payment.expired event.

http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);

Errors & Limits

Status When What to do
401 Missing or invalid API token Check the Authorization: Bearer header.
403 Account awaiting approval Wait for approval, or contact support.
403 {"success":false,"code":"ACCOUNT_FROZEN",…} — the account is temporarily restricted Contact PalmPesa Support.
404 Unknown order_id on the status endpoint Check the ID and the API token used.
409 transaction_id already used for a payment with different details Use a new transaction_id for a new payment.
422 Validation failed Read errors for the field and the message.
429 Rate limit exceeded Slow down and retry after a short wait.
500 Unexpected error Retry with the same transaction_id; you will not be charged twice.

Validation error 422

JSON
{
  "message": "The callback URL must be a publicly reachable HTTPS URL.",
  "errors": {
    "callback_url": ["The callback URL must be a publicly reachable HTTPS URL."]
  }
}

Rate limits

Endpoint Limit
POST /api/v2/async/payments 30 requests per minute
GET /api/v2/async/payments/{order_id} 120 requests per minute

Callbacks

When a payment reaches a final status, PalmPesa sends a POST request with a JSON body to the callback_url supplied when initiating payment. No callbacks are sent for INITIATING, PENDING, or AMBIGUOUS statuses.

Request Headers

HeaderAlwaysValue
Content-TypeYesapplication/json
X-PalmPesa-Event-IdYesSame value as event_id in the body
X-PalmPesa-TimestampOnly if signing is enabledUnix time in seconds when the callback was signed
X-PalmPesa-SignatureOnly if signing is enabledsha256= followed by a hex HMAC

Example Callback Body — payment.completed

{
  "event": "payment.completed",
  "event_id": "82c12c18a92fc6f3bfb61f89bbf0b8f10f7790a3cfe7b0940734eb43eca87eb1",
  "transaction_id": "ORDER-2026-000123",
  "order_id": "PALMPESAV2A20261008120000123EFGH5678",
  "selcom_reference": "0289999288",
  "status": "COMPLETED",
  "amount": 10000,
  "currency": "TZS",
  "channel": "TIGOPESATZ",
  "phone": "255712345678",
  "processed_at": "2026-10-08T12:01:31+03:00"
}

Events

EventStatusWhat to do
payment.completedCOMPLETEDPayment received. Fulfil the order.
payment.cancelledCANCELLEDThe customer cancelled. Do not fulfil.
payment.rejectedREJECTEDThe network rejected the payment. Do not fulfil.
payment.initiation_failedINITIATION_FAILEDThe prompt was never sent and nothing was charged. Retry with a new transaction_id if desired.
payment.expiredEXPIREDPayment was not received within about an hour. Do not fulfil based on this event alone; a late payment.completed callback may still arrive.
Use the order ID for reconciliation

Match callbacks to your records using order_id and transaction_id. Process callbacks idempotently using event_id, since delivery retries can occur.

Support & Resources

Our team is available to help you integrate PalmPesa into your platform.

Email Support

Technical questions & escalations

support@palmpesa.co.tz

24–48 hour response

WhatsApp

Fast responses during business hours

+255 693 662 424

Mon–Fri, 8AM–5PM EAT

Postman Collection

Request Payment and Payment Status, ready to send

Download collection

Import, set api_token, send. How to use it

Integration Checklist

Before you go live
  • Call the API from your server only; keep the token secret.
  • Generate a unique transaction_id per payment (for example a UUID) and store the returned order_id.
  • On timeouts, retry with the same transaction_id; for a new payment, use a new one.
  • Treat only COMPLETED as paid. Never fulfil on INITIATING, PENDING, AMBIGUOUS, EXPIRED or a 202.
  • Handle a payment.completed that arrives after payment.expired: the customer has paid.
  • Make your callback_url public HTTPS, answer 2xx within 10 seconds, and deduplicate on event_id.
  • Test end to end with a small real payment to your own phone.
Copied to clipboard