Skip to main content
Star-Pay sends real-time payment notifications to your server by POSTing a JSON payload to the callbackURL you specify when creating an order. To ensure these requests are authentic and unmodified, each callback is signed with HMAC-SHA256.

Overview

1

Provide a callbackURL when creating an order

Include the callbackURL field in your POST /trdp/order request body. This must be a publicly accessible HTTPS endpoint on your server.
{
  "callbackURL": "https://yoursite.com/payments/callback",
  ...
}
2

Receive the callback

After the customer completes (or fails) payment, Star-Pay POSTs the transaction result to your endpoint.
3

Verify the signature

Check the X-Signature header against the HMAC-SHA256 of the payload before trusting the data.
4

Process the payment

Once verified, update your database, fulfill the order, and return HTTP 200.

Callback payload

Star-Pay sends the following JSON body to your callbackURL.

Successful payment

{
  "billRefNo": "33WJ8946WB",
  "status": "PAID",
  "timestamp": "2025-12-10T11:05:37.566Z",
  "message": "Payment successful",
  "merchantId": "6888dc21ee7cbfe63657144f",
  "customerId": "656445e6-20fa-440d-b8c9-0a588d1ca05b",
  "externalReferenceId": "CLA9SR5XR9",
  "amount": 1000,
  "payment_type": "USSD_PUSH",
  "receipt_url": "https://receipt.starpayethiopia.com/receiptqa/WST-33WJ8946WB"
}

Failed payment

{
  "billRefNo": "5I974ZLE60",
  "status": "FAILED",
  "message": "Payment failed"
}

Payload fields

FieldTypeDescription
billRefNostringStar-Pay bill reference number
statusstringPAID or FAILED
timestampstringISO 8601 time of transaction completion
messagestringHuman-readable result message
merchantIdstringYour merchant identifier
customerIdstringCustomer identifier in Star-Pay’s system
externalReferenceIdstringExternal reference for reconciliation
amountnumberAmount charged
payment_typestringPayment method used, e.g. USSD_PUSH
receipt_urlstringLink to the payment receipt

Callback headers

Every callback request from Star-Pay includes these headers:
{
  "X-Signature": "79eb81c3d69395f5261dca9bc5f10079d54c49f8f40b1fc79f63635f3dbec3b8",
  "X-Timestamp": "1770748190504"
}
Your Webhook Secret is available in your merchant dashboard under Dashboard → Webhooks. This is a separate secret from your x-api-secret API key. Never expose it in frontend code or public repositories.

Signature verification

Algorithm

Star-Pay generates the signature as:
HMAC_SHA256(secret, "${timestamp}.${JSON.stringify(payload)}")
The timestamp is embedded in the signed message to prevent replay attacks. To verify:
  1. Extract X-Timestamp from the request headers.
  2. Extract X-Signature from the request headers.
  3. Recompute HMAC_SHA256(webhookSecret, "${X-Timestamp}.${JSON.stringify(body)}").
  4. Compare the computed signature to X-Signature using a timing-safe comparison.
  5. Reject the request if they do not match.

Signature verification code

signature.js
import crypto from "crypto";

/**
 * Create HMAC-SHA256 signature for a payload
 * Matches the signature used when sending the callback
 */
export function createSignature(
  payload: unknown,
  secret: string,
  timestamp: string
): string {
  const body = JSON.stringify(payload);
  const message = `${timestamp}.${body}`; // include timestamp to prevent replay
  return crypto.createHmac("sha256", secret).update(message).digest("hex");
}

/**
 * Verify incoming callback signature
 * @param payload - JSON payload received
 * @param timestamp - X-Timestamp header from request
 * @param signature - X-Signature header from request
 * @param secret - Merchant's callback secret
 */
export function verifySignature(
  payload: unknown,
  timestamp: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = createSignature(payload, secret, timestamp);

  const expectedBuffer = Buffer.from(expectedSignature, "hex");
  const signatureBuffer = Buffer.from(signature, "hex");

  if (expectedBuffer.length !== signatureBuffer.length) return false;

  // Timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
}

Handling the callback endpoint

The examples below show a complete webhook handler that verifies the signature and processes the payload.
server.js
import express from "express";
import { verifySignature } from "./signature";

const app = express();
app.use(express.json());

app.post("/callback", (req, res) => {
  const timestamp = req.header("X-Timestamp") as string;
  const signature = req.header("X-Signature") as string;

  if (!timestamp || !signature) {
    return res.status(400).json({ message: "Missing headers" });
  }

  const isValid = verifySignature(
    req.body,
    timestamp,
    signature,
    process.env.CALLBACK_SECRET as string
  );

  if (!isValid) {
    return res.status(401).json({ message: "Invalid signature" });
  }

  // Valid callback — process the payload
  const { billRefNo, status, amount } = req.body;

  if (status === "PAID") {
    // Fulfill the order associated with billRefNo
    console.log(`Payment received: ${billRefNo}, amount: ${amount}`);
  }

  res.status(200).json({ message: "Callback verified successfully" });
});

app.listen(3000, () => console.log("Server running on http://localhost:3000"));

Security notes

Always verify the HMAC signature before trusting or acting on a callback payload. An unverified callback could be forged by a malicious actor.
  • Use timing-safe comparison. Standard string equality (===) is vulnerable to timing attacks. Use crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or subtle.ConstantTimeCompare (Go).
  • Reject missing headers. Return HTTP 400 if X-Timestamp or X-Signature are absent.
  • Validate the timestamp window. Optionally reject callbacks where X-Timestamp is more than 5 minutes in the past to mitigate replay attacks.
  • Return 200 quickly. Acknowledge the callback immediately and process the payload asynchronously to avoid timeouts on Star-Pay’s side.
  • Never expose your webhook secret. Keep CALLBACK_SECRET in an environment variable, not in source code.

Summary

Security featurePurpose
HMAC-SHA256Ensures the payload was not modified in transit
Timestamp in signaturePrevents replay attacks
Timing-safe comparisonPrevents timing side-channel attacks
Shared webhook secretAuthenticates that the sender is Star-Pay

Build docs developers (and LLMs) love