A webhook is a message SavaPay sends to your server when something changes, such as a successful payment or completed refund. Use webhooks to start work in your system, such as fulfilment after payment succeeds.
Always verify the message
Before trusting a webhook, verify its signature with your webhook signing secret. This proves SavaPay sent it and prevents forged payment-success notices.
Headers
| Header | Meaning |
|---|---|
X-SavaPay-Signature | HMAC-SHA256 hexadecimal signature |
X-SavaPay-Event | Event type, such as payment.succeeded |
X-SavaPay-Event-Id | Unique event ID |
Signature scheme
signature = HMAC-SHA256(signingSecret, `${eventId}.${timestamp}.${rawBody}`)- signingSecret — the merchant webhook signing secret issued during onboarding.
- eventId — the X-SavaPay-Event-Id header.
- timestamp — the event createdAt field from the JSON body.
- rawBody — the exact raw request body as received; do not re-serialize parsed JSON.
Event payload
{
"id": "evt_1abc",
"type": "payment.succeeded",
"createdAt": "2026-09-14T00:15:00Z",
"data": { "paymentIntentId": "pi_1abc", "amount": 1299, "currency": "USD" }
}Handle the same message more than once
SavaPay may retry a webhook if your server does not respond successfully. Store each event ID after processing. If it arrives again, return success without repeating fulfilment.
Confirm before fulfilment
A webhook tells you to check a payment. Before shipping goods or delivering a service, ask SavaPay for the current status and confirm it is succeeded.
Respond quickly
Return a successful response as soon as you safely record the event. Handle longer work in the background.
Events to handle
- payment.created — record that SavaPay accepted the request.
- payment.succeeded — complete the order after confirming status.
- payment.failed — tell the customer payment did not complete.
- payment.expired — allow the customer to try again.
- payment.cancelled — stop the pending order.
- payment.refunded — update records and reverse fulfilment where needed.
Verification example (Node/Express)
const crypto = require('crypto')
app.post('/savapay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-savapay-signature']
const eventId = req.headers['x-savapay-event-id']
const rawBody = req.body.toString('utf8')
const event = JSON.parse(rawBody)
const expected = crypto
.createHmac('sha256', process.env.SAVAPAY_WEBHOOK_SECRET)
.update(`${eventId}.${event.createdAt}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signature || '')
const valid = a.length === b.length && crypto.timingSafeEqual(a, b)
if (!valid) return res.status(400).send('invalid signature')
// Handle the event by type.
switch (event.type) {
case 'payment.succeeded':
// Enqueue server-side status verification before fulfilment.
break
case 'payment.refunded':
// Reverse fulfilment.
break
}
res.status(200).send('ok')
})Retries
Failed deliveries are retried by a background worker. Handle events idempotently because the same event ID may arrive more than once.
Do not trust webhooks alone
Reconcile with GET /v1/payment-intents/{id}. Use webhooks to trigger work and the status endpoint to confirm it.