Skip to content

Authentication

Every Pesapal API call except RequestToken itself needs a bearer token. You exchange your consumer key and secret for one, then send it in the Authorization header.

Two things about this endpoint are worth knowing before you write any code:

  1. Get your consumer and secret keys from the Pesapal demo keys page. Sandbox credentials are published publicly, so you can start without an account.

  2. Exchange them 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"
    }'
  3. A successful response looks like this:

    {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI...",
    "expiryDate": "2026-09-03T12:08:08.5585879Z",
    "error": null,
    "status": "200",
    "message": "Request processed successfully"
    }
  4. Send the token as a bearer header on subsequent calls:

    Authorization: Bearer YOUR_ACCESS_TOKEN

expiryDate is exactly one hour after the request which you can confirm by decoding the JWT and substracting iat from exp to get 3600:

{
"uid": "qkio1BGGYAXTu2JOfm7XSXNruoZsrqEW",
"iat": 1788432786,
"exp": 1788436386,
"iss": "http://cybqa.pesapal.com/",
"aud": "http://cybqa.pesapal.com/"
}

exp minus iat is 3600.

Request one token and reuse it until shortly before it expires. Requesting a new token everytime is unnecessary:

let cached = null;
async function getToken() {
// Refresh a minute early, so a request in flight cannot expire mid-call.
if (cached && cached.expiresAt - 60_000 > Date.now()) {
return cached.token;
}
const res = await fetch(`${BASE_URL}/Auth/RequestToken`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
consumer_key: process.env.PESAPAL_CONSUMER_KEY,
consumer_secret: process.env.PESAPAL_CONSUMER_SECRET,
}),
});
const body = await res.json();
if (!body.token) throw new Error(body.error?.message ?? "Authentication failed");
// exp is seconds since epoch, and avoid parsing expiryDate
const { exp } = JSON.parse(atob(body.token.split('.')[1]));
cached = { token: body.token, expiresAt: exp * 1000 };
return cached.token;
}

Note the check on body.token rather than on res.ok or body.error. This API returns HTTP 200 for authentication failures, so the transport status tells you nothing. See Errors.

Getting a token fails with HTTP 200 and an error body:

Cause error_type code message
Wrong key or secret api_error invalid_consumer_key_or_secret_provided Invalid Access Credentials provided
Missing fields invalid_request_error invalid_api_request_parameters Consumer Key is required|Consumer Secret is required

Using a token in endpoints that require it fails differently, and inconsistently:

Cause HTTP Body
No Authorization header 401 error encoded as a JSON string inside message
Invalid or expired token 500 Standard error object, status: null

Three unrelated mistakes all produce a message about your credentials:

What you actually did wrong What the API says
Sent malformed JSON ||Consumer Key is required|Consumer Secret is required
Sent form-encoded instead of JSON Consumer Key is required|Consumer Secret is required
Genuinely omitted the fields Consumer Key is required|Consumer Secret is required

Only the third is honest. A JSON syntax error is not reported as a parse error, and a form-encoded body is accepted by content type and then silently fails to bind. In both cases the API tells you your credentials are missing when your credentials were fine.

Multiple validation errors arrive pipe-delimited in a single string rather than as an array, so splitting is up to you, and empty segments need filtering:

const problems = body.error.message.split('|').filter(Boolean);
// ["Consumer Key is required", "Consumer Secret is required"]

POST /Auth/RequestToken. Full schemas and examples are in the API reference.

Field Type Required Notes
consumer_key string Yes From the Pesapal demo keys page.
consumer_secret string Yes From the Pesapal demo keys page.

Content-Type: application/json is mandatory. Omitting it returns HTTP 415.

Field Type Notes
token string JWT. Send as Authorization: Bearer <token>.
expiryDate string UTC with a Z and seven fractional digits. One hour after issue.
error object or null null on success.
status string "200" on success. A quoted string, not a number.
message string "Request processed successfully" on success.
code Meaning
invalid_consumer_key_or_secret_provided The key or secret is wrong.
invalid_api_request_parameters Fields missing, or the body did not parse. See misleading errors.
invalid_api_credentials_provided The token you sent is missing or invalid. Returned by other endpoints, not this one.