PaySway signs each webhook request by computing an HMAC-SHA256 hash of the raw request body, concatenated with a timestamp. This hash is generated using a secret value that only you and PaySway know. The resulting signature is provided in the X-PaySway-Signature header. By replicating this same process with your secret, you can confirm that the webhook request is authentic and has not been tampered with.

1

Obtain your secret

When you create a subscription via PaySway API, the response includes a field called secret, which is base64-encoded. You must decode this string before using it to generate an HMAC signature.

const base64Secret = "zTOJGr3vYdAHM/F5ZiDsVvgPZq5/Y3Ktbo9xw9Ncf8Y=";
const decodedSecret = Buffer.from(base64Secret, "base64");
2

Locate the timestamp and signature

PaySway includes a X-PaySway-Signature header in each webhook request. This header contains two key-value pairs, separated by commas:

  • t: The UNIX timestamp of when the message was signed
  • v1: The actual signature in hexadecimal representation

Example Header

Signature header
X-PaySway-Signature: t=1738002855,v1=c9854765d242b9078e68b6fca1755f208ba70a7aa7c372abc4ec341483e34496

To verify the signature, you need to parse the header and extract the values for t and v1. Ignore any other values that may appear in the header.

const signatureHeader = "t=1738002855,v1=c9854765d242b9078e68b6fca1755f208ba70a7aa7c372abc4ec341483e34496";
const segments = signatureHeader.split(",");
const timestamp = segments.find(s => s.startsWith("t=")).substring(2);
const signature = segments.find(s => s.startsWith("v1=")).substring(3);
3

Reconstruct the signing payload

PaySway signs the combination of timestamp and raw request body joined with a dot. Make sure to use the exact raw JSON payload (including whitespace if any) as received from PaySway.

const rawBody = '{"foo":"bar"}'; 
const signingPayload = `${timestamp}.${rawBody}`;
4

Generate the expected signature

Use the decoded secret (raw bytes) from step 1, and compute the HMAC-SHA256 of the message constructed in step 3. The resulting hash should be converted to a hexadecimal string for comparison with v1.

import { createHmac } from "crypto";
// ...
const expectedSignature = createHmac("sha256", decodedSecret)
  .update(signingPayload, "utf8")
  .digest("hex");
5

Compare signatures and check timestamp

Finally, compare your expectedSignature to the v1 value found in the X-PaySway-Signature header:

  • If they match, it means the request was signed by PaySway using correct secret
  • If they do not match, you should treat the request as untrusted

You can also use the timestamp (t) to set a maximum acceptable age for the request (e.g., five minutes). If the timestamp is too far in the past or far in the future, it may indicate a replay attack or clock mismatch.