Webhooks

Webhooks

SaySwitch sends merchant webhooks to the webhook URL configured on the merchant dashboard.

Webhook requests are sent as HTTP POST requests with a JSON body.

Configure Webhooks

  1. Log into the SaySwitch Dashboard.
  2. Navigate to Settings > API Key > Webhook URL.
  3. Add your webhook endpoint URL.

Request Headers

HeaderDescription
payloadSignatureHMAC SHA512 signature of the raw JSON payload using the merchant secret key.
merchantSignatureHMAC SHA512 signature of the merchant public key using the merchant secret key.
timestampTimestamp when the webhook was sent.
Content-TypeAlways application/json.

Signature Rules

payloadSignature = hash_hmac("SHA512", raw_json_payload, merchant_secret_key)
merchantSignature = hash_hmac("SHA512", merchant_public_key, merchant_secret_key)

Use the correct keys for the transaction domain:

DomainKeys to use
liveLive secret key and live public key.
testTest secret key and test public key.

Body Structure

{
  "notify": "transaction | payout",
  "notifyType": "successful | failed",
  "data": {}
}

Supported Values

FieldSupported values
notifytransaction, payout

Current Active Notify Types

notifyTypeDescription
successfulSent when a transaction or payout completes successfully.
failedSent when a transaction or payout fails. Automatic failed payout reversal/refund is currently communicated through the failed payout webhook.
notifyTypeDescription
reversedRecommended structure for a dedicated reverse payout webhook once enabled. Manual admin reversed payout currently sets payout status to reversed and refunds the wallet, but does not currently send a separate merchant webhook.

Final Status Notifications

Pending status is generally not sent as a merchant webhook. A transaction or payout may be pending during processing, but SaySwitch sends webhook notifications when the final status becomes successful or failed.

Webhook Samples

{
  "notify": "transaction",
  "notifyType": "successful",
  "data": {
    "id": 10231,
    "business_id": 169,
    "currency": "NGN",
    "amount": "5000.00",
    "reference": "S2S_OPAY_WALLET_001",
    "ip_address": "102.89.44.10",
    "channel": "opay",
    "type": "banktransfer",
    "domain": "live",
    "fees": "75.00",
    "customer_id": 4512,
    "plan": null,
    "requested_amount": "5000.00",
    "status": "success",
    "card_attempt": null,
    "message": "Payment successful",
    "created_at": "2026-06-13T10:20:30.000000Z",
    "paid_at": "2026-06-13T10:22:15.000000Z",
    "customer": {
      "id": 4512,
      "email": "customer@example.com",
      "first_name": "Test",
      "last_name": "Customer",
      "customer_code": "CUS_ABC123456"
    }
  }
}
FieldValue
notifytransaction
notifyTypesuccessful
data.statussuccess

For dedicated account and bank transfer collections, transferDetails may be included when available.

For multiple-wallet collections, use the transaction reference to reconcile the wallet-specific payment.

Verifying Webhook Signatures

Always verify both payloadSignature and merchantSignature before processing a webhook.

Use the raw request body when generating the expected payloadSignature. Do not parse and re-stringify the JSON before verification.

import crypto from "crypto";
 
function verifySaySwitchWebhook({ rawBody, headers, secretKey, publicKey }) {
  const expectedPayloadSignature = crypto
    .createHmac("sha512", secretKey)
    .update(rawBody)
    .digest("hex");
 
  const expectedMerchantSignature = crypto
    .createHmac("sha512", secretKey)
    .update(publicKey)
    .digest("hex");
 
  const payloadSignature = headers["payloadsignature"];
  const merchantSignature = headers["merchantsignature"];
 
  return (
    payloadSignature === expectedPayloadSignature &&
    merchantSignature === expectedMerchantSignature
  );
}

Handling Webhooks

Return HTTP 200 after receiving and validating a webhook. Process long-running work asynchronously so your endpoint responds quickly.

Best Practices

#Recommendation
1Verify both webhook signatures before processing the payload.
2Use the raw JSON payload for signature verification.
3Use the key pair that matches the webhook domain.
4Treat webhook processing as idempotent using the transaction or payout reference.
5Respond with HTTP 200 promptly and process heavy tasks asynchronously.
6Validate notify, notifyType, and data.status before updating records.

Need help? Contact support at developers@sayswitch.com.