Skip to content

Quickstart

This page takes you from nothing to a completed sandbox payment. It should take you about ten minutes, and you do not need a Pesapal account to follow along.

The flow has five steps:

  1. Exchange your credentials for an access token
  2. Register a URL where Pesapal can notify your server
  3. Create an order and get a payment link
  4. Send the customer to that link
  5. Verify the payment actually completed

Pesapal publishes sandbox credentials openly, so grab them from the demo keys page.

All requests in this page use the sandbox base URL:

https://cybqa.pesapal.com/pesapalv3/api
  1. Exchange your consumer key and secret for a token.

    Terminal window
    curl --request POST \
    --url https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken \
    --header 'Accept: application/json' \
    --header 'Content-Type: application/json' \
    --data '{
    "consumer_key": "YOUR_CONSUMER_KEY",
    "consumer_secret": "YOUR_CONSUMER_SECRET"
    }'
  2. Save the token from the response. It’s valid for one hour, so cache it rather than requesting a new one per call.

    {
    "token": "eyJhbGciOiJIUzI1N....",
    "expiryDate": "2026-09-03T12:08:08.5585879Z",
    "error": null,
    "status": "200",
    "message": "Request processed successfully"
    }

Pesapal calls this URL when an order changes. You register it once and reuse the id it returns on every order.

  1. Register your endpoint. For now, a request bin from webhook.site works and requires no setup.

    Terminal window
    curl --request POST \
    --url https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN \
    --header 'Accept: application/json' \
    --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
    --header 'Content-Type: application/json' \
    --data '{
    "url": "https://example.com/pesapal/ipn",
    "ipn_notification_type": "GET"
    }'
  2. Save the ipn_id from the response. That value is your notification_id in the next step.

    {
    "url": "https://webhook.site/c83....",
    "created_date": "2026-09-03T11:29:08.61",
    "ipn_id":"28f0d4...",
    "notification_type": 0,
    "ipn_notification_type_description": "GET",
    "ipn_status": 1,
    "ipn_status_decription": "Active",
    "status":"200",
    "message":"Request processed successfully"
    }
Terminal window
curl --request POST \
--url https://cybqa.pesapal.com/pesapalv3/api/Transactions/SubmitOrderRequest \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"id": "ORDER-1234",
"currency": "KES",
"amount": 100,
"description": "Test Payment",
"callback_url": "https://example.com/payment-complete",
"notification_id": "YOUR_IPN_ID",
"billing_address": {
"email_address": "customer@example.com"
}
}'
Field Required Notes
id Yes Your own reference. Must be unique, maximum 50 characters.
currency Yes ISO code, for example KES.
amount Yes Number or string. Must be greater than zero.
description Yes Shown to the customer.
callback_url Yes Where the customer’s browser lands after paying.
notification_id Yes The ipn_id from step 2.
billing_address Yes Must be present. May be an empty object.

Your response should be similar to the following:

{
"order_tracking_id": "523a3fb1-568b-4c53-b9ff-d9f2c4866653",
"merchant_reference": "ORDER-1234",
"redirect_url": "https://cybqa.pesapal.com/pesapaliframe/PesapalIframe3/Index?OrderTrackingId=523a3fb1-...",
"error": null,
"status": "200"
}

Store order_tracking_id against your order. It is how you look the payment up later.

Redirect the customer to redirect_url. They choose a payment method and pay using a test card:

Card Number Expiry CVV
Visa, approves 4761 7390 0101 0010 07/28 123

Once the payment is successful, the customer lands back on your callback_url with two query parameters appended:

https://example.com/payment-complete?OrderTrackingId=523a3fb1-...&OrderMerchantReference=ORDER-1234

This is the step that determines whether the money moved.

Terminal window
curl --request GET \
--url 'https://cybqa.pesapal.com/pesapalv3/api/Transactions/GetTransactionStatus?orderTrackingId=YOUR_ORDER_TRACKING_ID' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Your response should be similar to the following:

{
"payment_method": "Visa",
"amount": 100,
"confirmation_code": "7884396530786173704004",
"payment_status_description": "Completed",
"status_code": 1,
"merchant_reference": "ORDER-1234",
"currency": "KES"
}

status_code: 1 means completed, and 2 means the payment failed. That is the only field you should act on.

You have taken a payment, but not yet built something you could run in production. Three things are missing:

  • Handle the notification. Polling works for a demo. Real integrations act on the IPN, which is the only signal that reaches you if the customer closes the tab. See Instant Payment Notifications.
  • Handle errors properly. This API returns HTTP 200 for failures and has eight distinct response shapes. See Errors.
  • Cache your token. It lasts an hour, not the five minutes Pesapal documents. See Authentication.