Integration guides
Advanced features
Open in Markdown

Real-time notifications

Stay informed about payment events with real-time webhooks. Automate your order processing, and more.

Overview

Webhooks allow you to receive real-time notifications about events that occur in your Reservepay account. Instead of polling our API to check for updates, your server can listen for incoming requests from Reservepay.

Setting up webhooks

To start receiving webhook notifications, connect a webhook endpoint to your account. You can manage webhooks through the API or from your merchant dashboard under Developer Settings.

You can connect several endpoints to one account. Each endpoint has its own destination_url, events to subscribe to, signature_type, and api_version. When an event happens, every active endpoint subscribed to it gets its own copy.

Currently supported events

Event Description
payment_authorized Triggered when a payment status transitions to AUTHORIZED.
payment_completed Triggered when a payment status transitions to SUCCESSFUL or FAILED.
payment_expired Triggered when a payment status transitions to EXPIRED.
payment_voided Triggered when a payment status transitions to VOIDED.
payment_reversed Triggered when a payment status transitions to REVERSED.
payout_completed Triggered when a payout status transitions to SUCCESSFUL or FAILED.
topup_settled Triggered when a topup status transitions to SUCCESSFUL.

payment_completed and payout_completed fire for both success and failure. Always read the status in the payload before you fulfill an order or mark a payout as paid. For what each status means, see Payment lifecycle.

Track refunds and disputes by polling, since they don't have events. Poll refunds with find-refund (see Refunds, voids, and reversals) and disputes with list-disputes (see Disputes).

Handling webhook requests

When an event occurs, Reservepay sends a POST request to your destination URL with a JSON payload.

Payload

Payment and payout deliveries include the following fields. See the Webhook reference for each event's complete payload:

  • event_id: A unique ID for the event, starting with evt_. It stays the same across retries and across endpoints, so use it to spot duplicates.
  • status: The payment or payout status when the event happened.
  • delivered_at: When this delivery attempt was sent, as a Unix timestamp. It changes on every retry, so two deliveries of the same event have different bodies and signatures. Spot duplicates with event_id, not with a hash of the body.
  • external_id: Your own reference, if you set one.

Verifying signatures

Every delivery is signed, so you can confirm it came from Reservepay. Check the signature before you trust the event. You choose the signature type when you connect the endpoint, and you can find the endpoint's verification_key in the dashboard.

Check the signature against the body exactly as you received it, before you parse it. Parsing and re-encoding the JSON changes the bytes and breaks the signature. Reject a request whose signature is missing or doesn't match.

Signature type Headers
HMAC_SHA256 Reservepay-Signature
ED25519 Reservepay-Signature
SHA256 X-Webhook-Signature

The header value is prefixed with the scheme name, in the form <scheme>=<hex digest> (for example hmac_sha256=<hex>).

HMAC (SHA256)

Compute an HMAC-SHA256 digest of the raw request body using your verification key (Base64-decoded) and compare it to the hex value after hmac_sha256= in the Reservepay-Signature header.

This Node.js function does the check:

import { createHmac, timingSafeEqual } from "node:crypto"

function verifyReservepaySignature(rawBody, header, verificationKey) {
  const [scheme, signature] = (header ?? "").split("=")
  if (scheme !== "hmac_sha256" || !signature) return false

  const expected = createHmac("sha256", Buffer.from(verificationKey, "base64"))
    .update(rawBody)
    .digest("hex")

  const received = Buffer.from(signature)
  const computed = Buffer.from(expected)
  return received.length === computed.length && timingSafeEqual(received, computed)
}

Pass it the body exactly as it arrived. In Express, read the body with express.raw() rather than express.json(), then parse the JSON only after the signature checks out:

app.post("/webhooks/reservepay", express.raw({ type: "application/json" }), async (req, res) => {
  const valid = verifyReservepaySignature(
    req.body,
    req.get("Reservepay-Signature"),
    process.env.RESERVEPAY_WEBHOOK_KEY,
  )
  if (!valid) return res.sendStatus(401)

  const event = JSON.parse(req.body.toString("utf8"))
  await saveEventOnce(event) // Your code: store the event, skipping any event_id you already have.
  res.sendStatus(200)
})

Ed25519

Verify the hex-decoded signature after ed25519= in the Reservepay-Signature header against the raw request body using your verification key (Base64-decoded Ed25519 public key). Use a libsodium-compatible library for verification.

SHA256

For this legacy scheme, append the verification_key string exactly as returned by the API to the raw JSON body, with no separator. Compute the SHA256 hex digest of those bytes and compare it to the value after sha256= in the X-Webhook-Signature header. Do not Base64-decode the key for this scheme.

Choose HMAC_SHA256 or ED25519 for new endpoints.

Responding to webhooks

Return any 2xx status within 10 seconds. Reservepay ignores the response body. To stay within the time limit:

  1. Check the signature.
  2. Save the event, skipping it if you already have its event_id.
  3. Return 200 OK.
  4. Do the actual work, such as fulfilling the order, in a background job.

If a duplicate arrives for an event you already saved, return 200 OK again so Reservepay stops retrying.

Retries and ordering

If your endpoint times out, can't be reached, or returns anything other than 2xx, Reservepay retries with increasing delays, up to 20 attempts in total. Because of retries, the same event can arrive more than once, and events can arrive out of order.

Don't use arrival order or delivered_at to decide which status is newer. When you need the current status, call find-payment or find-payout.

Checking delivery history

To see whether Reservepay reached your endpoint, call list-payment-activities with a payment_id, or list-payout-activities with a payout_id. Each event in the response has an event_deliveries array. Each entry covers one endpoint and one outcome:

  • url: The endpoint Reservepay called.
  • status_code: The HTTP status your endpoint returned, or 0 if it didn't respond.
  • failure_code: Why the delivery failed, or null if it succeeded.
  • occurrences: How many attempts ended with this outcome.
  • payload: The body that was sent.

An event can have both failed and successful entries for the same endpoint when a retry succeeded. The event was delivered if any entry for that endpoint has failure_code set to null.

Recovering missed events

After 20 failed attempts, Reservepay stops retrying the event, and deliveries can't be resent. If your endpoint was down for longer than the retry window, compare your records with the API instead, as described in Recovering missed updates.

Rotating the signing key

An endpoint keeps the same verification_key for as long as it exists. To move to a new HMAC_SHA256 key:

  1. Connect a second endpoint with merchants/connect-webhook, using the same destination URL, and store its verification_key.
  2. While both endpoints are active, try each key and accept the request if either one matches. Every event is delivered to both endpoints with the same event_id, so your deduplication handles the overlap.
  3. When deliveries to the new endpoint are succeeding, remove the old one through the dashboard.

ED25519 endpoints all verify with Reservepay's public key, so connecting a new endpoint doesn't give you a new key.

Integrate further

Dive deeper into these advanced integration topics to unlock Reservepay's full potential with our in-depth guides.

Refunds, voids, and reversals

Issue refunds, void transactions before they settle, and reverse authorizations when needed.

Read the guide
Disputes

Find disputed payments, submit evidence, and respond before the deadline.

Read the guide
Authorization and capture

Separate authorization from capture to hold funds without charging. Perfect for pre-orders or reservations

Read the guide
Working with Terminal

Build your own POS, or work with our app by sending commands or using deep links.

Read the guide
Customer intelligence

Build profiles by attaching customer data. Gain insights, reduce fraud risk, and create experiences that drive loyalty.

Read the guide
Sending funds

Send payouts to your primary bank account or to your contacts via bank account, mobile number or national ID.

Read the guide
Real-time notifications

Stay informed about payment events with real-time webhooks. Automate your order processing, and more...

Read the guide
Payment lifecycle

Check payment statuses, available actions, refunds, and disputes.

Read the guide
Reconciliation

Filter payments and payouts, export CSVs, read balance holds, and reconcile daily statements.

Read the guide
Payment failure codes

Look up what each Reservepay failure code means when a payment is declined.

Read the guide

No account yet?

Start integrating all these amazing features into your app or website by creating your own merchant account today. It's free to sign up and only takes a few minutes to get started.