Skip to content

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.

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.

MethodPOST
Content typeapplication/json
Authorization headerYour application’s integration key
BodyThe transaction result, as plain JSON
{
"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": []
}
FieldTypeDescription
referenceNumberstringPesepay’s reference for the transaction — match this to your order
dateOfTransactionstringWhen the transaction was created
applicationIdnumberThe Pesepay application the transaction belongs to
applicationNamestringThat application’s name
amountDetailsobjectAmounts and fees — see below
reasonForPaymentstringThe reason you supplied when creating the transaction
transactionStatusstringThe final status
transactionStatusCodenumberNumeric equivalent of transactionStatus
transactionStatusDescriptionstringHuman-readable description of the status
resultUrlstringThe callback URL this was sent to
returnUrlstringWhere the customer was returned to
pollUrlstringURL for checking the status again
transactionMetadataobjectString key/value pairs carried on the transaction. On an application with split payments this also carries the splitAmountMode and splitPrincipalAmount Pesepay writes
splitsarrayThe legs of an EcoCash payment collected in parts — always empty on the callback. Despite the name it has nothing to do with split payments
FieldTypeDescription
amountnumberThe amount charged, in currencyCode
currencyCodestringCurrency of the transaction
defaultCurrencyAmountnumberamount converted to the platform’s default currency
defaultCurrencyCodestringThe platform’s default currency code
transactionServiceFeenumberThe Pesepay service fee
customerPayableAmountnumberWhat the customer actually pays
totalTransactionAmountnumberTotal amount for the transaction
merchantAmountnumberWhat you receive
  1. Return 2xx quickly. Do slow work — emails, fulfilment, invoicing — asynchronously.

  2. Confirm before you fulfil. Call Check Payment Status with the referenceNumber and act on that response. See Verifying callbacks for why.

  3. Be idempotent. Key your handler on referenceNumber so a repeated delivery can’t ship an order twice or credit a wallet twice.

  4. Check transactionStatus, not just that a callback arrived. A callback fires for failed and reversed payments too — only SUCCESS means you were paid.

  5. Don’t rely on the callback alone. Failed deliveries are not retried; run a reconciliation job as described in Retries & reliability.

This is the whole shape: reject anything not carrying your integration key, acknowledge immediately, then confirm and fulfil out of band.

payments/result.js
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);
});

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.