Skip to main content

Verify webhook signatures

SkyPay signs the exact request body with your endpoint secret and sends:

X-SkyPay-Signature: sha256=<hex HMAC>
X-SkyPay-Event: payment.success
X-SkyPay-Delivery: <delivery id>

Signature verification must use the raw body bytes. Parsing and re-serializing JSON can change whitespace or key ordering and invalidate an otherwise legitimate signature.

Express
const crypto = require('node:crypto')
const express = require('express')
const app = express()

function verifyWebhook(rawBody, signature, secret) {
if (!signature?.startsWith('sha256=')) return false

const expected = Buffer.from(
`sha256=${crypto.createHmac('sha256', secret).update(rawBody).digest('hex')}`,
'utf8'
)
const received = Buffer.from(signature, 'utf8')

return (
expected.length === received.length &&
crypto.timingSafeEqual(expected, received)
)
}

app.post(
'/webhooks/skypay',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.header('x-skypay-signature')
if (
!verifyWebhook(req.body, signature, process.env.SKYPAY_WEBHOOK_SECRET)
) {
return res.status(400).send('Invalid signature')
}

const event = JSON.parse(req.body.toString('utf8'))
await storeEventOnce(req.header('x-skypay-delivery'), event)
return res.status(204).send()
}
)

Do not use ordinary string equality for signatures. Timing-safe comparison reduces information leakage. Reject missing, malformed, or mismatched signatures before invoking any business logic.

Use a separate secret for each Sandbox and Live endpoint. Provider signatures are verified internally by SkyPay and are not the signature your merchant handler should validate.