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 webhook subscription, the response includes a secret field that 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

Extract the timestamp and signature

PaySway includes an 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 format

Example Header

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

Parse the header to extract the t and v1 values. 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 the timestamp and raw request body, separated by a period

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

Do not parse or modify the request body before verification. Use the raw, unmodified payload exactly as received, preserving all whitespace and formatting.

4

Generate the expected signature

Use your webhook secret to compute the HMAC-SHA256 hash of the signing payload. Convert the resulting hash to a hexadecimal string for comparison.

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

Verify the signature and timestamp

Compare your expectedSignature with the v1 value from the X-PaySway-Signature header:

  • If they match: The request is authentic and was signed by PaySway using the correct secret
  • If they don’t match: Reject the request as potentially malicious or corrupted

Additionally, use the timestamp t to implement replay attack protection by setting a maximum acceptable age for requests (e.g., 5 minutes).