Razorpay Integration
Receive and route Razorpay webhooks for payments, refunds, subscriptions and settlements.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Razorpay Production",
"slug": "razorpay",
"provider": "razorpay",
"signingSecret": "your-razorpay-webhook-secret"
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/razorpay2. Configure the Razorpay Webhook
- Go to Razorpay Dashboard → Settings → Webhooks
- Click Add New Webhook
- Paste your Hookbase ingest URL
- Enter a Secret — this is a value you choose, and it is the key Razorpay signs with
- Select the events you want to subscribe to
- Save, then store the same secret on your Hookbase source as
signingSecret
See Razorpay's webhook documentation for the full event list.
Info
Razorpay's webhook secret is set by you, not generated. Use a long random value, use a different one per environment, and keep test-mode and live-mode webhooks on separate Hookbase sources so a test-mode payment never routes into production handlers.
3. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Payments Service", "slug": "razorpay-payments", "url": "https://api.myapp.com/webhooks/razorpay"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Razorpay to Payments", "sourceId": "src_...", "destinationId": "dst_..."}'Signature Verification
Razorpay signs webhooks with HMAC-SHA256 over the raw request body. The hex digest is sent in the
X-Razorpay-Signature header, with no prefix:
X-Razorpay-Signature: 9c1f7d3b5a2e4086c1a3e5d7f9b0a2c4e6d8f0a1b3c5d7e9f1a3b5c7d9e1f3a5Hookbase verifies this automatically once the source has both fields set:
{
"provider": "razorpay",
"signingSecret": "your-razorpay-webhook-secret"
}The key is the webhook secret you entered in the dashboard — not your Razorpay API key secret, which is a different credential. There is no timestamp in this scheme, so nothing expires and nothing needs a clock tolerance.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored.
Common Events
| Event | Description |
|---|---|
payment.authorized | A payment was authorized |
payment.captured | A payment was captured |
payment.failed | A payment failed |
refund.created | A refund was initiated |
refund.processed | A refund was processed |
subscription.activated | A subscription was activated |
subscription.charged | A subscription was charged |
settlement.processed | A settlement was processed |
Payment Captured
{
"entity": "event",
"account_id": "acc_1234567890",
"event": "payment.captured",
"contains": [
"payment"
],
"payload": {
"payment": {
"entity": {
"id": "pay_1234567890",
"amount": 50000,
"currency": "INR",
"status": "captured",
"method": "upi"
}
}
},
"created_at": 1678901234
}Info
amount is in the smallest currency unit — 50000 is ₹500.00, not ₹50,000. Divide by 100 before
displaying it or writing it to a ledger.
Refund Created
{
"entity": "event",
"account_id": "acc_1234567890",
"event": "refund.created",
"contains": [
"refund"
],
"payload": {
"refund": {
"entity": {
"id": "rfnd_1234567890",
"payment_id": "pay_1234567890",
"amount": 50000,
"currency": "INR",
"status": "processed"
}
}
},
"created_at": 1678902000
}Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
Ledger Row
function transform(payload) {
const kind = payload.contains && payload.contains.length
? payload.contains[0]
: null;
const entity = kind && payload.payload[kind]
? payload.payload[kind].entity
: {};
return {
event: payload.event,
account_id: payload.account_id,
entity_type: kind,
entity_id: entity.id || null,
payment_id: entity.payment_id || entity.id || null,
amount_major: typeof entity.amount === "number" ? entity.amount / 100 : null,
currency: entity.currency || null,
status: entity.status || null,
method: entity.method || null,
occurred_at: payload.created_at
};
}Slack Alert on Failed Payments
function transform(payload) {
const p = payload.payload.payment ? payload.payload.payment.entity : {};
return {
text: `Razorpay payment failed: ${p.id}`,
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Payment:*\n${p.id}` },
{ type: "mrkdwn", text: `*Amount:*\n${(p.amount || 0) / 100} ${p.currency || ""}` },
{ type: "mrkdwn", text: `*Method:*\n${p.method || "unknown"}` }
]
}
]
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
Captured Payments Only
{
"name": "Captured Payments",
"logic": "AND",
"conditions": [
{
"field": "event",
"operator": "equals",
"value": "payment.captured"
}
]
}High-Value Payments
{
"name": "High Value",
"logic": "AND",
"conditions": [
{
"field": "event",
"operator": "equals",
"value": "payment.captured"
},
{
"field": "payload.payment.entity.amount",
"operator": "greater_than",
"value": "1000000"
}
]
}Any Refund
{
"name": "Refunds",
"logic": "AND",
"conditions": [
{
"field": "event",
"operator": "starts_with",
"value": "refund."
}
]
}Headers
| Header | Description |
|---|---|
X-Razorpay-Signature | Hex HMAC-SHA256 of the raw body, no prefix |
Hookbase does not forward these headers to your destination. A delivery is a fresh request:
Hookbase sets Content-Type, User-Agent: Hookbase/1.0, X-Delivery-ID and X-Event-ID, then
adds the headers and auth you configured on the destination. Nothing Razorpay sent reaches your
handler as a header — read what you need out of the body, or set it on the destination yourself.
Of the incoming headers, only content-type, user-agent, x-github-event, x-gitlab-event and
stripe-signature are stored on the event, so those are the only ones visible later in the
dashboard or the API.
Troubleshooting
Signature Verification Failed
- Confirm the source's
providerisrazorpay— a source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256, and Razorpay sendsX-Razorpay-Signature - Confirm
signingSecretis the webhook secret from the dashboard, not the API key secret - Test mode and live mode have separate webhook configurations and separate secrets — check you are comparing like with like
Missing Events
- Check the event is selected on the webhook in Settings → Webhooks
- Check the webhook is active and pointed at the right ingest URL
- Razorpay retries failed deliveries; a destination that was down will usually backfill
Duplicate Events
Hookbase has no provider event-id extractor for Razorpay, so the default auto dedup strategy falls
back to hashing the payload. For an idempotency key that survives retries, use the entity id from
payload.<entity>.entity.id in your handler.