The result callback
The resultUrl you set when creating a transaction
(Initiate Transaction or
Make Payment) is where Pesepay POSTs the transaction’s
final result. Use it to mark orders paid without polling — but always
confirm the result before you act on it, as described in
Verifying callbacks.
When it fires
Section titled “When it fires”Once, when the transaction reaches a
terminal status — SUCCESS, FAILED,
CANCELLED, REVERSED, and so on. Non-terminal statuses like PENDING,
PROCESSING, and PARTIALLY_PAID do not produce a callback, so a
silent endpoint doesn’t mean the payment is fine.
The request
Section titled “The request”| Method | POST |
| Content type | application/json |
Authorization header | Your application’s integration key |
| Body | The transaction result, as plain JSON |
Payload
Section titled “Payload”{ "referenceNumber": "20260901103214123-A1B2C3D4", "dateOfTransaction": "2026-09-01T10:32:14.000+00:00", "applicationId": 1234, "applicationName": "Your Application", "amountDetails": { "amount": 10.0, "currencyCode": "USD", "defaultCurrencyAmount": 10.0, "defaultCurrencyCode": "USD", "transactionServiceFee": 0.3, "customerPayableAmount": 10.3, "totalTransactionAmount": 10.3, "merchantAmount": 10.0 }, "reasonForPayment": "Order #1042 — running shoes", "transactionStatus": "SUCCESS", "transactionStatusCode": 304, "transactionStatusDescription": "Transaction was successfully completed", "resultUrl": "https://example.com/payments/result", "returnUrl": "https://example.com/payments/return", "pollUrl": "https://api.pesepay.com/api/payments-engine/v1/payments/check-payment?referenceNumber=20260901103214123-A1B2C3D4", "transactionMetadata": {}, "splits": []}| Field | Type | Description |
|---|---|---|
referenceNumber | string | Pesepay’s reference for the transaction — match this to your order |
dateOfTransaction | string | When the transaction was created |
applicationId | number | The Pesepay application the transaction belongs to |
applicationName | string | That application’s name |
amountDetails | object | Amounts and fees — see below |
reasonForPayment | string | The reason you supplied when creating the transaction |
transactionStatus | string | The final status |
transactionStatusCode | number | Numeric equivalent of transactionStatus |
transactionStatusDescription | string | Human-readable description of the status |
resultUrl | string | The callback URL this was sent to |
returnUrl | string | Where the customer was returned to |
pollUrl | string | URL for checking the status again |
transactionMetadata | object | String key/value pairs carried on the transaction. On an application with split payments this also carries the splitAmountMode and splitPrincipalAmount Pesepay writes |
splits | array | The legs of an EcoCash payment collected in parts — always empty on the callback. Despite the name it has nothing to do with split payments |
amountDetails
Section titled “amountDetails”| Field | Type | Description |
|---|---|---|
amount | number | The amount charged, in currencyCode |
currencyCode | string | Currency of the transaction |
defaultCurrencyAmount | number | amount converted to the platform’s default currency |
defaultCurrencyCode | string | The platform’s default currency code |
transactionServiceFee | number | The Pesepay service fee |
customerPayableAmount | number | What the customer actually pays |
totalTransactionAmount | number | Total amount for the transaction |
merchantAmount | number | What you receive |
Handling it
Section titled “Handling it”-
Return
2xxquickly. Do slow work — emails, fulfilment, invoicing — asynchronously. -
Confirm before you fulfil. Call Check Payment Status with the
referenceNumberand act on that response. See Verifying callbacks for why. -
Be idempotent. Key your handler on
referenceNumberso a repeated delivery can’t ship an order twice or credit a wallet twice. -
Check
transactionStatus, not just that a callback arrived. A callback fires for failed and reversed payments too — onlySUCCESSmeans you were paid. -
Don’t rely on the callback alone. Failed deliveries are not retried; run a reconciliation job as described in Retries & reliability.
A complete handler
Section titled “A complete handler”This is the whole shape: reject anything not carrying your integration key, acknowledge immediately, then confirm and fulfil out of band.
const express = require('express');const { Pesepay } = require('pesepay');
const INTEGRATION_KEY = process.env.PESEPAY_INTEGRATION_KEY;const pesepay = new Pesepay(INTEGRATION_KEY, process.env.PESEPAY_ENCRYPTION_KEY);
const app = express();
app.post('/payments/result', express.json(), async (req, res) => { // Pesepay sends your own integration key back. Anything else is not us. if (req.get('authorization') !== INTEGRATION_KEY) { return res.sendStatus(401); }
const { referenceNumber } = req.body;
// Acknowledge before doing any work — a failed delivery is never retried. res.sendStatus(200);
const order = await orders.findByReference(referenceNumber); if (!order) return; // not ours — ignore it if (order.status === 'paid') return; // already handled — idempotent
// The callback body proves nothing. Ask Pesepay directly. const result = await pesepay.checkPayment(referenceNumber); if (!result.success || !result.paid) return;
await orders.markPaid(order.id); await fulfil(order);});import osfrom flask import Flask, requestfrom pesepay import Pesepay
INTEGRATION_KEY = os.environ["PESEPAY_INTEGRATION_KEY"]pesepay = Pesepay(INTEGRATION_KEY, os.environ["PESEPAY_ENCRYPTION_KEY"])
app = Flask(__name__)
@app.post("/payments/result")def payment_result(): # Pesepay sends your own integration key back. Anything else is not us. if request.headers.get("Authorization") != INTEGRATION_KEY: return "", 401
reference_number = request.get_json(force=True).get("referenceNumber")
# Queue the work, then acknowledge — a failed delivery is never retried. queue.enqueue(settle_order, reference_number) return "", 200
def settle_order(reference_number): order = orders.find_by_reference(reference_number) if order is None: # not ours — ignore it return if order.status == "paid": # already handled — idempotent return
# The callback body proves nothing. Ask Pesepay directly. result = pesepay.check_payment(reference_number) if not result.success or not result.paid: return
orders.mark_paid(order.id) fulfil(order)<?phprequire_once 'vendor/autoload.php';use Codevirtus\Payments\Pesepay;
$integrationKey = getenv('PESEPAY_INTEGRATION_KEY');
// Pesepay sends your own integration key back. Anything else is not us.if (($_SERVER['HTTP_AUTHORIZATION'] ?? '') !== $integrationKey) { http_response_code(401); exit;}
$body = json_decode(file_get_contents('php://input'), true);$referenceNumber = $body['referenceNumber'] ?? '';
// Acknowledge before doing any work — a failed delivery is never retried.http_response_code(200);header('Content-Length: 0');if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request();}
$order = find_order_by_reference($referenceNumber);if (!$order || $order['status'] === 'paid') { exit; // not ours, or already handled — idempotent}
// The callback body proves nothing. Ask Pesepay directly.$pesepay = new Pesepay($integrationKey, getenv('PESEPAY_ENCRYPTION_KEY'));$response = $pesepay->checkPayment($referenceNumber);
if ($response->success() && $response->paid()) { mark_order_paid($order['id']); fulfil_order($order);}This handler covers a callback that arrives. It does nothing for one that
doesn’t, which is the other half of the job — pair it with the
reconciliation job in Retries & reliability, pointing
at the same settle_order function.