Webhooks (or callbacks) let you automatically receive asynchronous events from Paycashless when important events happen — like when a payment status changes or a virtual account gets credited.

Register your webhook URLs

Webhooks are currently registered when you call APIs via the callbackUrl field.

Webhook Structure

All webhooks have the same payload structure.

  • event : the name of the event
  • data : event data specific to the event being sent

Webhook Retries

Always acknowledge a webhook instantly by responding with 2xx, else it will be considered as failed. Failed webhooks are retried with constant backoff for a maximum of 3 times with a delay of 1 minute.

Webhook Security

To ensure you are receiving webhooks from Paycashless we provide Request-Signature and Request-Timestamp in the webhook request header. This is similar to the API signing request, but in reverse. You MUST validate the signature to ensure it’s originating from Paycashless.

Webhook signature is signed using HMAC SHA-512 with your API secret as the signing key. Request-Signature and Request-Timestamp will be provided in the webhook request header for you to reconstruct the message for verification.

To verify the signature follow the process below:

StepActionDescription
1HashTake the event data object, stringify the object and hash it with HMAC SHA-512 algorithm using your API secret as the signing key (output should be hex-encoded).
2ConcatenateConcatenate your full callback url exactly as you provided it, hashed event data, and Request-Timestamp from the header. There are no spaces or other characters between these values. The order of the fields must follow the order stipulated here.
3SignTake the string from the Concatenate step and generate a HMAC SHA-512 signature using your API secret as the signing key.
4EncodeTake the output of the Sign step and hex-encode it.
5VerifyCompare the recreated signature with the content of Request-Signature header.

The fields used to generate the signature are as follows. If the conditions below are not met, you won’t be able to recreate the Request-Signature header.

FieldDescription
Callback URLLowercased full URL as provided with the base url and search parameters (e.g. https://yourwebsite.com/callback/paycashless?notify=all).
Hashed BodyThe event data object, stringified and hashed using HMAC SHA-512 algorithm.
TimestampThe value gotten from Request-Timestamp header.

Study the node.js webhook verification example code below:

verify_webhook_signature.js
import { createHmac, timingSafeEqual } from 'crypto';

function sha512Sign(message, secret) {
  return createHmac('sha512', secret).update(message).digest('hex');
}

export function sha512Verify(data: string, signature: string, key: string): boolean {
  // Generate the expected HMAC
  const expectedSignature = createHmac('sha512', key)
    .update(data, 'utf8')
    .digest('hex');
  
  // Convert both signatures to buffers for timing-safe comparison
  const expectedBuffer = Buffer.from(expectedSignature, 'hex');
  const providedBuffer = Buffer.from(signature, 'hex');
  
  // Ensure both buffers are the same length before comparison
  if (expectedBuffer.length !== providedBuffer.length) {
    return false;
  }
  
  // Use timing-safe comparison to prevent timing attacks
  return timingSafeEqual(expectedBuffer, providedBuffer);
}

// Get the timestamp from 'Request-Timestamp' header
const requestTimestamp = "1704931925543";
const callbackUrl = "https://webhook.site/e1ae9397-bd8d-4327-ad92-da002ea2ef08";
const API_SECRET = "YOUR_API_SECRET"; // replace these with your actual API secret value 
const requestSignature = "f17e737bf9d4a1f46f130126be4b2cdb3109a347565b5f824f9da0a45f14663db899812654d849d731cf825b884e1363a8364c5e622ec86f338a31fdeec7812d";
const event = {"event":"events.payout.succeeded","data":{"amount":10000,"callbackUrl":"https://webhook.site/e1ae9397-bd8d-4327-ad92-da002ea2ef08","confirmationState":"confirmed","currency":"NGN","destinationAccountNumber":"9845648577","destinationBankId":"bank_538ed2056326432ba8e6853b613997bb","emtl":0,"failureCode":null,"failureMessage":null,"fee":1500,"id":"po_dtb9z9jk4fs6vqelh3hb8dxcyscnldpx","liveMode":true,"metadata":{"category":"transfer"},"narration":"zapped","networkSessionId":"090286250606000850470781628636","reference":"trx_fww7b31pbs5mmT3k3qfb47","requiresConfirmation":false,"status":"succeeded","vat":0}}

const bodyHash = sha512Sign(JSON.stringify(event.data), API_SECRET);
const stringToSign = `${callbackUrl}${bodyHash}${timestamp}`;

// Generate the Request-Signature value for the header
const isValid = sha512Verify(stringToSign, requestSignature, API_SECRET);

console.log(isValid ? "Signature is valid" : "Signature is not valid");