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:
- Exchange your credentials for an access token
- Register a URL where Pesapal can notify your server
- Create an order and get a payment link
- Send the customer to that link
- Verify the payment actually completed
Prerequisites
Section titled “Prerequisites”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/api1. Get an Access token
Section titled “1. Get an Access token”-
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"}'const url = 'https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken';const options = {method: 'POST',headers: {Accept: 'application/json', 'Content-Type': 'application/json'},body: '{"consumer_key":"YOUR_CONSUMER_KEY","consumer_secret":"YOUR_CONSUMER_SECRET"}'};try {const response = await fetch(url, options);const data = await response.json();console.log(data);} catch (error) {console.error(error);}import requestsurl = "https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken"payload = {"consumer_key": "YOUR_CONSUMER_KEY","consumer_secret": "YOUR_CONSUMER_SECRET"}headers = {"Accept": "application/json","Content-Type": "application/json"}response = requests.post(url, json=payload, headers=headers)print(response.json())<?php$curl = curl_init();curl_setopt_array($curl, [CURLOPT_URL => "https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken",CURLOPT_RETURNTRANSFER => true,CURLOPT_ENCODING => "",CURLOPT_MAXREDIRS => 10,CURLOPT_TIMEOUT => 30,CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,CURLOPT_CUSTOMREQUEST => "POST",CURLOPT_POSTFIELDS => json_encode(['consumer_key' => 'YOUR_CONSUMER_KEY','consumer_secret' => 'YOUR_CONSUMER_SECRET']),CURLOPT_HTTPHEADER => ["Accept: application/json","Content-Type: application/json"],]);$response = curl_exec($curl);$err = curl_error($curl);curl_close($curl);if ($err) {echo "cURL Error #:" . $err;} else {echo $response;}package mainimport ("fmt""strings""net/http""io")func main() {url := "https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken"payload := strings.NewReader("{\n \"consumer_key\": \"YOUR_CONSUMER_KEY\",\n \"consumer_secret\": \"YOUR_CONSUMER_SECRET\"\n}")req, _ := http.NewRequest("POST", url, payload)req.Header.Add("Accept", "application/json")req.Header.Add("Content-Type", "application/json")res, _ := http.DefaultClient.Do(req)defer res.Body.Close()body, _ := io.ReadAll(res.Body)fmt.Println(res)fmt.Println(string(body))} -
Save the
tokenfrom 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"}
2. Register a Notification URL
Section titled “2. Register a Notification URL”Pesapal calls this URL when an order changes. You register it once and reuse the id it returns on every order.
-
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"}'const url = 'https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN';const options = {method: 'POST',headers: {Authorization: 'Bearer YOUR_ACCESS_TOKEN',Accept: 'application/json','Content-Type': 'application/json'},body: '{"url":"https://example.com/pesapal/ipn","ipn_notification_type":"GET"}'};try {const response = await fetch(url, options);const data = await response.json();console.log(data);} catch (error) {console.error(error);}import requestsurl = "https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN"payload = {"url": "https://example.com/pesapal/ipn","ipn_notification_type": "GET"}headers = {"Authorization": "Bearer YOUR_ACCESS_TOKEN","Accept": "application/json","Content-Type": "application/json"}response = requests.post(url, json=payload, headers=headers)print(response.json())<?php$curl = curl_init();curl_setopt_array($curl, [CURLOPT_URL => "https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN",CURLOPT_RETURNTRANSFER => true,CURLOPT_ENCODING => "",CURLOPT_MAXREDIRS => 10,CURLOPT_TIMEOUT => 30,CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,CURLOPT_CUSTOMREQUEST => "POST",CURLOPT_POSTFIELDS => json_encode(['url' => 'https://example.com/pesapal/ipn','ipn_notification_type' => 'GET']),CURLOPT_HTTPHEADER => ["Accept: application/json","Authorization: Bearer YOUR_ACCESS_TOKEN","Content-Type: application/json"],]);$response = curl_exec($curl);$err = curl_error($curl);curl_close($curl);if ($err) {echo "cURL Error #:" . $err;} else {echo $response;}package mainimport ("fmt""strings""net/http""io")func main() {url := "https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN"payload := strings.NewReader("{\n \"url\": \"https://example.com/pesapal/ipn\",\n \"ipn_notification_type\": \"GET\"\n}")req, _ := http.NewRequest("POST", url, payload)req.Header.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN")req.Header.Add("Accept", "application/json")req.Header.Add("Content-Type", "application/json")res, _ := http.DefaultClient.Do(req)defer res.Body.Close()body, _ := io.ReadAll(res.Body)fmt.Println(res)fmt.Println(string(body))} -
Save the
ipn_idfrom the response. That value is yournotification_idin 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"}
3. Create an Order
Section titled “3. Create an Order”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" }}'const url = 'https://cybqa.pesapal.com/pesapalv3/api/Transactions/SubmitOrderRequest';const options = { method: 'POST', headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN', Accept: 'application/json', 'Content-Type': 'application/json' }, body: '{"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"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}import requests
url = "https://cybqa.pesapal.com/pesapalv3/api/Transactions/SubmitOrderRequest"
payload = { "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" }}headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json", "Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.json())<?php
$curl = curl_init();
curl_setopt_array($curl, [ CURLOPT_URL => "https://cybqa.pesapal.com/pesapalv3/api/Transactions/SubmitOrderRequest", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_POSTFIELDS => json_encode([ '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' ] ]), CURLOPT_HTTPHEADER => [ "Accept: application/json", "Authorization: Bearer YOUR_ACCESS_TOKEN", "Content-Type: application/json" ],]);
$response = curl_exec($curl);$err = curl_error($curl);
curl_close($curl);
if ($err) { echo "cURL Error #:" . $err;} else { echo $response;}package main
import ( "fmt" "strings" "net/http" "io")
func main() {
url := "https://cybqa.pesapal.com/pesapalv3/api/Transactions/SubmitOrderRequest"
payload := strings.NewReader("{\n \"id\": \"ORDER-1234\",\n \"currency\": \"KES\",\n \"amount\": 100,\n \"description\": \"Test Payment\",\n \"callback_url\": \"https://example.com/payment-complete\",\n \"notification_id\": \"YOUR_IPN_ID\",\n \"billing_address\": {\n \"email_address\": \"customer@example.com\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(res) fmt.Println(string(body))
}| 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.
4. Send the Customer to the Payment Page
Section titled “4. Send the Customer to the Payment Page”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-12345. Verify the Payment
Section titled “5. Verify the Payment”This is the step that determines whether the money moved.
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'const url = 'https://cybqa.pesapal.com/pesapalv3/api/Transactions/GetTransactionStatus?orderTrackingId=YOUR_ORDER_TRACKING_ID';const options = { method: 'GET', headers: {Authorization: 'Bearer YOUR_ACCESS_TOKEN', Accept: 'application/json'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}import requests
url = "https://cybqa.pesapal.com/pesapalv3/api/Transactions/GetTransactionStatus"
querystring = {"orderTrackingId":"YOUR_ORDER_TRACKING_ID"}
headers = { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Accept": "application/json"}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())<?php
$curl = curl_init();
curl_setopt_array($curl, [ CURLOPT_URL => "https://cybqa.pesapal.com/pesapalv3/api/Transactions/GetTransactionStatus?orderTrackingId=YOUR_ORDER_TRACKING_ID", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_HTTPHEADER => [ "Accept: application/json", "Authorization: Bearer YOUR_ACCESS_TOKEN" ],]);
$response = curl_exec($curl);$err = curl_error($curl);
curl_close($curl);
if ($err) { echo "cURL Error #:" . $err;} else { echo $response;}package main
import ( "fmt" "net/http" "io")
func main() {
url := "https://cybqa.pesapal.com/pesapalv3/api/Transactions/GetTransactionStatus?orderTrackingId=YOUR_ORDER_TRACKING_ID"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer YOUR_ACCESS_TOKEN") req.Header.Add("Accept", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(res) fmt.Println(string(body))
}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.
What next
Section titled “What next”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.