Webhooks
GrowthBnk POSTs a signed event to your configured URL when a payment reaches a terminal state — verify it, then fulfill.
Setup
Configure your webhook URL in the GrowthBnk dashboard and ensure webhooks are enabled for your account.
GrowthBnk sends an HTTP POST with a JSON body; respond with 200 to acknowledge.
Any non-200 response (or a timeout) is retried.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
| Content-Type | string | Required | Always application/json. |
| key | string | Required | Your merchant API key — confirm it matches your account. |
| hash | string | Required | SHA-256 signature of the event (verify this). |
Signature
The hash header is computed exactly like an API request signature:
hash = SHA256( key | transaction_id | transaction_amount | salt )
Compute the expected hash using the transaction_amount exactly as it appears in the payload (a two-decimal string, e.g. "1499.00"). A formatting mismatch will fail verification.
Verify a webhook
Recompute the hash with your salt and compare it to the hash header using a constant-time comparison
before trusting the payload:
import hashlib
def verify_webhook(headers, payload, api_key, api_salt):
signing = f"{api_key}|{payload['transaction_id']}|{payload['transaction_amount']}|{api_salt}"
expected = hashlib.sha256(signing.encode("utf-8")).hexdigest()
# Constant-time compare against the `hash` header
import hmac
if not hmac.compare_digest(expected, headers.get("hash", "")):
raise ValueError("Invalid webhook signature")
return payload<?php
function verify_webhook(array $headers, array $payload, string $apiKey, string $apiSalt): array {
$signing = "{$apiKey}|{$payload['transaction_id']}|{$payload['transaction_amount']}|{$apiSalt}";
$expected = hash('sha256', $signing);
$received = $headers['hash'] ?? '';
if (!hash_equals($expected, $received)) {
throw new RuntimeException('Invalid webhook signature');
}
return $payload;
}const crypto = require('crypto');
function verifyWebhook(headers, payload, apiKey, apiSalt) {
const signing = `${apiKey}|${payload.transaction_id}|${payload.transaction_amount}|${apiSalt}`;
const expected = crypto.createHash('sha256').update(signing).digest('hex');
const received = headers['hash'] || '';
const ok = expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!ok) throw new Error('Invalid webhook signature');
return payload;
}Payload
A representative payment_webhook event:
{
"event_type": "payment_webhook",
"transaction_id": "TXN20260707172038123",
"merchant_transaction_id": "ORDER-2026-0001",
"transaction_status": "SUCCESS",
"transaction_amount": "1499.00",
"currency": "INR",
"transaction_date": "2026-07-07T17:20:38Z",
"timestamp": "2026-07-07T17:20:41Z",
"is_refund": false,
"customer_details": {
"customer_name": "Jane Doe",
"customer_email": "jane.doe@example.com",
"customer_phone": "9876543210"
},
"merchant_details": {
"merchant_id": "MID-10234",
"merchant_name": "Example Store"
},
"fees": {
"commission_charges": "0.00",
"commission_tax": "0.00",
"service_charges": "0.00",
"service_tax": "0.00"
}
}Key fields
| Field | Type | Description |
|---|---|---|
| event_type | string | Event name, e.g. payment_webhook. |
| transaction_id | string | GrowthBnk transaction id. |
| merchant_transaction_id | string | Your reference for the payment. |
| transaction_status | string | One of INITIATED, SUCCESS, FAILURE, DROP, CANCELLED. |
| transaction_amount | string | Amount in rupees (two decimals) — used in the signature. |
| currency | string | Currency code. |
| transaction_date | string | ISO-8601 time the payment was created. |
| timestamp | string | ISO-8601 time the webhook was sent. |
| is_refund | boolean | Whether the event relates to a refund. |
| customer_details | object | Customer name, email and phone. |
| merchant_details | object | Your merchant id and name. |
| fees | object | Commission and service charges/taxes. |
Best Practices
- Always verify the
hashbefore acting — never trust an unverified payload. - Respond
200quickly; do heavy work asynchronously so GrowthBnk does not retry. - Make your handler idempotent — the same event may be delivered more than once.
- Reconcile against Transaction Status as a backstop if a webhook is missed.