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
- Log into the SaySwitch Dashboard.
- Navigate to Settings > API Key > Webhook URL.
- Add your webhook endpoint URL.
Request Headers
| Header | Description |
|---|---|
payloadSignature | HMAC SHA512 signature of the raw JSON payload using the merchant secret key. |
merchantSignature | HMAC SHA512 signature of the merchant public key using the merchant secret key. |
timestamp | Timestamp when the webhook was sent. |
Content-Type | Always 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:
| Domain | Keys to use |
|---|---|
live | Live secret key and live public key. |
test | Test secret key and test public key. |
Body Structure
{
"notify": "transaction | payout",
"notifyType": "successful | failed",
"data": {}
}Supported Values
| Field | Supported values |
|---|---|
notify | transaction, payout |
Current Active Notify Types
notifyType | Description |
|---|---|
successful | Sent when a transaction or payout completes successfully. |
failed | Sent when a transaction or payout fails. Automatic failed payout reversal/refund is currently communicated through the failed payout webhook. |
Recommended / Coming Soon Notify Types
notifyType | Description |
|---|---|
reversed | Recommended 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"
}
}
}| Field | Value |
|---|---|
notify | transaction |
notifyType | successful |
data.status | success |
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 |
|---|---|
| 1 | Verify both webhook signatures before processing the payload. |
| 2 | Use the raw JSON payload for signature verification. |
| 3 | Use the key pair that matches the webhook domain. |
| 4 | Treat webhook processing as idempotent using the transaction or payout reference. |
| 5 | Respond with HTTP 200 promptly and process heavy tasks asynchronously. |
| 6 | Validate notify, notifyType, and data.status before updating records. |
Need help? Contact support at developers@sayswitch.com.