Skip to main content

Webhooks

Payrail uses webhooks to notify your application when asynchronous events occur. Instead of polling the API for status updates, register a webhook endpoint to receive real-time event notifications.


How webhooks work

Payrail API

Event occurs (e.g. transaction status changes to completed)

Payrail signs the payload with your webhook secret

Payrail sends POST request to your webhook URL

Your server verifies the signature and processes the event

When an event occurs, Payrail sends an HTTP POST request to your registered webhook URL with a signed JSON payload describing the event.


Event types

EventTrigger
payment.succeededTransaction status updated to completed
payment.failedTransaction status updated to failed
refund.processedRefund status updated to processed
refund.rejectedRefund status updated to rejected

Webhook payload

All webhook events share the same payload structure.

FieldTypeDescription
eventstringThe event type (e.g. payment.succeeded)
created_atstringISO 8601 timestamp of when the event occurred
dataobjectThe resource object associated with the event

Example payload — payment.succeeded

{
"event": "payment.succeeded",
"created_at": "2026-03-18T12:46:55.681Z",
"data": {
"_id": "69ba9d3e199bf8e79a8050e7",
"customer": "69ba9a90199bf8e79a8050e1",
"paymentMethod": "69ba9cef199bf8e79a8050e4",
"amount": 15000,
"currency": "USD",
"status": "completed",
"description": "Webhook test payment",
"createdAt": "2026-03-18T12:40:30.061Z"
}
}

Example payload — payment.failed

{
"event": "payment.failed",
"created_at": "2026-03-18T12:46:55.681Z",
"data": {
"_id": "69ba9d3e199bf8e79a8050e7",
"customer": "69ba9a90199bf8e79a8050e1",
"paymentMethod": "69ba9cef199bf8e79a8050e4",
"amount": 15000,
"currency": "USD",
"status": "failed",
"description": "Webhook test payment",
"createdAt": "2026-03-18T12:40:30.061Z"
}
}

Example payload — refund.processed

{
"event": "refund.processed",
"created_at": "2026-03-18T12:46:55.681Z",
"data": {
"_id": "69ba9d3e199bf8e79a8050e7",
"customer": "69ba9a90199bf8e79a8050e1",
"transaction": "69ba9d3e199bf8e79a8050e7",
"amount": 15000,
"reason": "Customer requested refund",
"status": "processed",
"createdAt": "2026-03-18T12:40:30.061Z"
}
}

Responding to webhooks

Your webhook endpoint must return a 200 OK response within 5 seconds to acknowledge receipt. If Payrail does not receive a 200 response, it will retry the webhook up to 3 times with exponential backoff.

Example response

{
"received": true
}

Retry policy

AttemptDelay
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours

After 3 failed attempts the event is marked as undelivered.


Verifying signatures

Payrail signs all webhook payloads using HMAC-SHA256. Always verify the signature before processing an event to confirm the request came from Payrail and has not been tampered with.

The signature is included in the X-Payrail-Signature request header:

X-Payrail-Signature: sha256=abc123...

The signature is generated by computing an HMAC-SHA256 hash of the raw request body using your webhook secret.

Important: Always verify the signature against the raw request body — not the parsed JSON object. Parsing the body before verification will cause signature checks to fail.

Verify in Node.js

const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(rawBody);
const expected = `sha256=${hmac.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}

// Express example
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-payrail-signature'];
const isValid = verifySignature(req.body, signature, process.env.WEBHOOK_SECRET);

if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

const event = JSON.parse(req.body);

switch (event.event) {
case 'payment.succeeded':
// fulfil order, send confirmation email, etc.
break;
case 'payment.failed':
// notify customer, retry logic, etc.
break;
case 'refund.processed':
// update order status, notify customer, etc.
break;
}

res.json({ received: true });
});

Verify in Python

import hmac
import hashlib

def verify_signature(raw_body, signature, secret):
hmac_obj = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
)
expected = f"sha256={hmac_obj.hexdigest()}"
return hmac.compare_digest(expected, signature)

# Flask example
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Payrail-Signature')
is_valid = verify_signature(request.get_data(), signature, WEBHOOK_SECRET)

if not is_valid:
return jsonify({'error': 'Invalid signature'}), 401

event = request.get_json()

if event['event'] == 'payment.succeeded':
pass # fulfil order, send confirmation email, etc.
elif event['event'] == 'payment.failed':
pass # notify customer, retry logic, etc.
elif event['event'] == 'refund.processed':
pass # update order status, notify customer, etc.

return jsonify({'received': True})