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})