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 Structures

Transaction and payout webhooks use notify and notifyType:

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

Refund webhooks use an event name that includes the refund status:

{
  "event": "refund.awaiting_settlement | refund.pending | refund.processing | refund.success | refund.failed | refund.rejected",
  "data": {}
}

Supported Values

FieldSupported values
notifytransaction, payout
eventrefund.awaiting_settlement, refund.pending, refund.processing, refund.success, refund.failed, refund.rejected

Current Active Notify Types

notifyTypeDescription
successfulSent when a transaction or payout completes successfully.
failedSent when a transaction or payout fails. A failed payout webhook can also indicate that the payout amount was returned to the merchant wallet.

Final Status Notifications

Pending status is generally not sent for transaction or payout webhooks. Refunds use a separate lifecycle and send refund.awaiting_settlement, refund.pending, refund.processing, refund.success, refund.failed, and refund.rejected events as their status changes.

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.

See Refund Webhooks for the full refund lifecycle and handler examples in JavaScript, PHP, Python, and Java.

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 originating test or live environment.
4Process transaction, payout, and refund webhooks idempotently using their references and event status.
5Respond with HTTP 200 promptly and process heavy tasks asynchronously.
6Validate notify and notifyType, or the refund event, together with data.status before updating records.

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