Skip to main content
When Star-Pay sends a callback to your server after a payment event, it signs the request with an HMAC-SHA256 signature. Verifying this signature ensures:
  • The request came from Star-Pay, not an unknown third party.
  • The payload was not modified in transit.
  • The request is not a replay of an earlier legitimate request.
Your Webhook Secret is available in Dashboard → Webhooks. Keep it secure and never expose it in frontend code or public repositories.

Callback headers

Every callback request from Star-Pay includes two required headers:
{
  "headers": {
    "X-Signature": "79eb81c3d69395f5261dca9bc5f10079d54c49f8f40b1fc79f63635f3dbec3b8",
    "X-Timestamp": "1770748190504"
  }
}
HeaderDescription
X-SignatureHMAC-SHA256 hex digest of the signed message
X-TimestampUnix millisecond timestamp when the callback was sent

Signature algorithm

The signature is computed as:
HMAC_SHA256(secret, "${timestamp}.${JSON.stringify(payload)}")
The timestamp is concatenated with the serialized payload body before hashing. This binds the signature to a specific point in time, preventing replay attacks.

Verification flow

1

Extract the headers

Read X-Timestamp and X-Signature from the incoming request headers. Reject the request immediately if either header is missing.
2

Serialize the payload

Serialize the request body to a JSON string using the same format as the sender (no extra spaces).
3

Build the signed message

Concatenate the timestamp and the serialized body: "${timestamp}.${serializedBody}".
4

Recompute the expected signature

Compute HMAC-SHA256 of the signed message using your Webhook Secret as the key. Encode the result as a lowercase hex string.
5

Compare signatures

Use a constant-time comparison function to compare the expected signature against the X-Signature header value. Reject the request if they do not match.
Optionally, also validate that the X-Timestamp is within an acceptable window (for example, ±5 minutes of your server clock) to further reduce replay attack surface.

Code examples

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);
}

Express.js webhook handler example

The following example shows a complete callback endpoint in Express.js using the verifySignature function above:
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 payload here
  console.log("Payload received:", req.body);

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

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

Security summary

FeaturePurpose
HMAC-SHA256Ensures data integrity
TimestampPrevents replay attacks
timingSafeEqualPrevents timing attacks
Shared SecretAuthenticates sender

Build docs developers (and LLMs) love