SavaPay Developers

Webhooks

Verify signed events, handle retries without duplicate work, and confirm final payment status.

BuildNode.js and ExpressApproved merchant pilot
Browse developer documentationWebhooks

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

HeaderMeaning
X-SavaPay-SignatureHMAC-SHA256 hexadecimal signature
X-SavaPay-EventEvent type, such as payment.succeeded
X-SavaPay-Event-IdUnique event ID

Signature scheme

text
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

json
{
  "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)

javascript
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.

Maintained byPayments Engineering

Last reviewed

Was this page helpful?