Skip to main content

Webhook Development

This page covers developer-facing details for building against PayRequest’s webhook — signature verification in more than one language, local testing, and how to build a resilient receiver given the delivery model. Start with Webhook Configuration first if you haven’t set up your webhook URL yet — this page assumes you already have a URL and signing secret configured.
PayRequest currently sends one event, payment.succeeded, to a single webhook URL per account. Everything below builds on that.

Signature Verification in Other Languages

Every request carries an X-PayRequest-Signature header formatted as sha256={hmac} — an HMAC-SHA256 of the raw request body, signed with your webhook secret. The PHP example is in Webhook Configuration; here’s the same check in Node.js and Python. Node.js (Express)
const crypto = require('crypto');

app.post('/webhooks/payrequest', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-payrequest-signature'] || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.PAYREQUEST_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  const sigBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);

  if (sigBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // handle event.data...
  res.status(200).send('ok');
});
Python (Flask)
import hashlib
import hmac

@app.route('/webhooks/payrequest', methods=['POST'])
def payrequest_webhook():
    signature = request.headers.get('X-PayRequest-Signature', '')
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(), request.data, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return 'Invalid signature', 401

    event = request.get_json()
    # handle event['data']...
    return 'ok', 200
In every language, compare signatures with a constant-time comparison function (hmac.compare_digest, crypto.timingSafeEqual, PHP’s hash_equals) — never ==/===, which leaks timing information.

Idempotency

With a single event type today, duplicate-processing risk is lower than on platforms with dozens of event types, but it’s still worth guarding against — a network retry on your own infrastructure, a webhook.site relay, or a manual reconciliation script could all deliver the same payment.succeeded event more than once.
  • Use data.id (the PayRequest transaction ID) as your idempotency key
  • Before acting on an event, check whether you’ve already processed that transaction ID
  • Because delivery isn’t automatically retried by PayRequest (see below), you’re more likely to see a missing event than a duplicate one — but your handler should tolerate both

Delivery Model (Recap)

As covered in Webhook Configuration: dispatch is asynchronous via a queued job, requests time out after 10 seconds, and delivery is best-effort — there’s no automatic retry if your endpoint is down or errors. Don’t build an integration that assumes every payment will arrive as a webhook; reconcile periodically against the API or dashboard as a safety net, especially for anything revenue-critical.

Testing Locally

There’s no built-in test-send button, so the practical way to develop against the webhook is to trigger a real payment (testmode where your provider supports it) and inspect what arrives.
1

Expose your local server

Use ngrok (ngrok http 3000) or a similar tunnel to get a public HTTPS URL pointing at your local dev server.
2

Or inspect payloads first with webhook.site

Before wiring up real handler code, point your webhook URL at a fresh webhook.site endpoint to see the exact payload shape and headers PayRequest sends.
3

Set the URL in PayRequest

Go to Settings → API Tokens, paste your ngrok or webhook.site URL, and save.
4

Trigger a payment

Complete a real (or testmode) payment against your account and confirm the request arrives, the signature verifies, and your handler processes it correctly.
5

Switch back to your production URL

Remember to update the webhook URL back to your real endpoint before going live — there’s only one URL per account, so a URL left pointed at a tunnel or webhook.site stops your production integration from receiving events.

FAQ

Queue it. Your endpoint has 10 seconds to respond before PayRequest’s request times out — do the minimum work needed to acknowledge receipt (verify signature, store/queue the event) and process the business logic asynchronously on your side.
Not currently — there’s only one event type (payment.succeeded), so there’s nothing to filter yet.
A 2xx status code to acknowledge receipt. Since PayRequest doesn’t automatically retry failed deliveries, returning an error won’t trigger a redelivery — it will just be logged as failed on PayRequest’s side.

Next Steps

Webhook Configuration

Set up your webhook URL and see the full payload reference

API Reference

Reconcile transaction state directly instead of relying solely on webhooks