Webhooks
Webhooks push events to a URL you register so your app doesn't have to poll. Every delivery is signed with HMAC-SHA256 using the secret returned when you created the webhook — verify it before trusting any payload.
Available events
| Event | When it fires |
|---|---|
| loan.application.submitted | Fired when a customer completes a loan application through your integration. |
| loan.application.approved | Fired on partner-lender approval. |
| loan.application.disbursed | Fired when funds hit the customer's bank. |
| kyc.verification.completed | Fired after KYC PAN+Aadhaar hard match completes. |
| insurance.policy.issued | Fired when the insurer issues the policy document. |
| payment.success | Fired for a successful EMI or subscription collection. |
| rate_limit.hit | Fired the moment your key hits the per-minute burst ceiling. |
| quota.exhausted | Fired the moment your monthly quota is fully consumed. |
Payload shape
Every event uses this envelope; the `data` object varies per event and mirrors the underlying resource.
{
"event": "loan.application.approved",
"id": "evt_9K3f...",
"occurred_at": "2026-02-01T12:34:56Z",
"data": {
"application_id": "app_ab12cd34",
"customer_id": "cus_5F8k9",
"product": "personal_loan",
"sanctioned_amount": 850000,
"partner_lender": "hdfc_bank"
}
}Verifying the signature
Every request carries an X-L4B-Signature: sha256=<hex> header. Compute HMAC-SHA256 over the raw request body using your webhook secret and constant-time compare.
import crypto from "crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
// constant-time compare
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader.replace(/^sha256=/, ""))
);
}
app.post("/webhooks/l4b", (req, res) => {
if (!verify(req.rawBody, req.get("X-L4B-Signature"), process.env.WEBHOOK_SECRET)) {
return res.status(401).send("bad signature");
}
// handle req.body.event
res.status(200).send("ok");
});import hmac, hashlib
def verify(raw_body: bytes, sig_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
provided = sig_header.replace("sha256=", "")
return hmac.compare_digest(expected, provided)Retry semantics
- Any non-2xx response (or a timeout > 10s) is treated as a failed delivery.
- Failed deliveries retry with exponential backoff:
30s → 2m → 10m → 1h → 6h → 12h → 24h. - After 7 attempts spanning ~44 hours the delivery is marked
failed_permanentand the webhook is auto-disabled. - Retries are idempotent — use the top-level
idfield to de-dupe on your side.
Test a webhook locally
The dashboard Test event button dispatches a stub payload for the event of your choice — perfect for wiring up your endpoint before events flow in production.
# Or fire it from the CLI
curl -X POST 'https://api.loan4bank.com/v3/marketplace/developers/webhooks/<id>/test' \
-H 'X-Dev-Session: <session>' \
-H 'Content-Type: application/json' \
-d '{"event":"loan.application.submitted"}'Everything you need to build a rock-solid webhook integration
LOAN4BANK webhooks push real-time notifications for every material event on your account — loan applications submitted and approved, KYC completed, EMI paid or missed, gold-loan prices refreshed, wallet ledger movements, referral commissions accrued, corporate gift redemptions and more. You subscribe to the events you care about, we sign every payload with HMAC-SHA256, and your endpoint decides how to react. Because webhooks are push-based they eliminate the need for polling loops, drop end-to-end latency to under a second, and keep your infrastructure costs predictable as your customer base grows.
Signature verification is not optional. Every request carries an `X-L4B-Signature` header whose value is the hex-encoded HMAC-SHA256 of the raw request body using your endpoint's signing secret. Compute the digest yourself and compare in constant time using your language's `hmac.compare_digest` (or equivalent) helper — never string equality, which is vulnerable to timing attacks. If the signature does not match, respond with 401 and log the incident; a well-behaved endpoint will only ever parse the body after the signature check succeeds.
Because networks fail, we retry every non-2xx response with exponential backoff — seven attempts spanning about 44 hours. Retries are idempotent, meaning the same event id can be redelivered any number of times, so build your handler to detect duplicates using the top-level `id` field and short-circuit if it has already been processed. Persist the event id in a database with a unique index, or use an in-memory cache with a 48-hour TTL if you're strictly stateless. The dashboard's Test event button and the CLI snippet above let you simulate deliveries locally through ngrok or Cloudflare Tunnel, so you can exercise your retry logic before ever taking real traffic.
For very high-throughput integrations, we recommend two additional patterns. First, terminate the webhook as fast as possible: return 2xx from the HTTP handler and push the payload onto a queue (SQS, RabbitMQ, Kafka) for asynchronous processing. Second, enable payload verification on both the HMAC AND the top-level `occurred_at` timestamp — reject any event older than five minutes to prevent replay attacks. Both patterns are shown in the SDK-specific snippets in the sidebar.
Frequently asked questions
How do I verify a LOAN4BANK webhook signature?
What events does LOAN4BANK send via webhooks?
How many times will LOAN4BANK retry a failed webhook delivery?
Can I test webhooks locally before going live?
How do I make my webhook handler idempotent?
Prefer async? Terminate the webhook handler in under 200ms by returning 2xx immediately and pushing the payload onto a queue for downstream processing. This is the pattern our largest partners use to process millions of events per day.