UPI Payment Webhooks Explained for Developers (Event Shapes, Delivery, Debugging)
VyaparGateway Team
Payments Editorial
Webhooks are the heartbeat of any modern UPI integration. They're the mechanism that lets your application know — without polling, without manual reconciliation — that a payment has actually landed. This guide is the developer reference for webhooks: every event type you'll receive, the exact shape of each payload, the delivery semantics you need to design around, and the debugging tools that will save you hours when something goes wrong in production.
Event types you'll receive
VyaparGateway emits a small, sharp set of events. You only need to handle the ones relevant to your flow, but knowing all of them helps when something unexpected shows up in your logs.
- intent.created — fired when your backend creates a new payment intent. Useful for audit logs; not usually load-bearing.
- intent.paid — fired when a customer successfully pays the intent. This is the event your application reacts to (mark order paid, fulfil, send receipt).
- intent.expired — fired when an intent's expiry window passes without payment. Useful for cleaning up abandoned carts in your UI.
- intent.cancelled — fired if you explicitly cancel an intent via the API. Mirror in your DB to keep state consistent.
- refund.created — fired when you initiate a refund. Useful for kicking off customer notifications.
- refund.completed — fired when the refund credit successfully lands in the customer's account.
- settlement.completed — fired when a batch of payments settles to your merchant bank account (relevant for PSP-batched setups).
For 90% of integrations, the only event that drives real work is intent.paid. The others are observability data.
Payload shape
Every webhook payload follows a consistent envelope:
{ id: 'evt_abc123', type: 'intent.paid', created_at: 1716100800, data: { intent_id: 'int_xyz789', order_id: 'ORDER12345', amount: 249900, currency: 'INR', status: 'paid', utr: '423456789012', payer_vpa: 'customer@bank', paid_at: 1716100795, metadata: { ...your custom fields... } } }
Fields to remember:
- id — the event ID. Stable across retries, so it makes a good idempotency key.
- type — the event type string (e.g. 'intent.paid').
- created_at — Unix timestamp when the event was generated (in seconds).
- data.order_id — your order ID as you passed it during intent creation. Use this to look up the order in your DB.
- data.utr — the UPI Unique Transaction Reference. Matches what appears in your bank statement.
- data.metadata — any custom fields you attached during intent creation. Useful for passing context through without storing it gateway-side.
Delivery semantics: at-least-once
VyaparGateway delivers webhooks with at-least-once semantics. That means: a given event will be delivered to your endpoint at least once, but possibly more than once. Your handler must be idempotent.
Retry behavior:
- First delivery happens within ~2 seconds of the underlying event.
- If your endpoint returns non-2xx or times out (default timeout: 10 seconds), we retry with exponential backoff.
- Retries happen at 1m, 5m, 15m, 1h, 6h, 24h, 72h after the first failure.
- After 7 days of unsuccessful delivery, the event is moved to a dead-letter queue you can manually replay from the dashboard.
- Successful delivery requires a 2xx response within the timeout window.
"Returning 200 OK before your DB commit completes will lose events. The gateway considers 200 as a successful delivery and stops retrying. If your process crashes between the 200 and the commit, the event is gone."
Local development: tunneling carefully
Webhooks need a public HTTPS URL to reach your endpoint, which is awkward when you're developing on localhost. Options:
- ngrok — fastest to set up. Generates a temporary HTTPS URL that forwards to your local port. Use a separate webhook secret for dev so a leaked tunnel URL can't be misused.
- Cloudflare Tunnel — free, more permanent, works behind firewalls. Setup is slightly more involved but worth it for ongoing dev.
- VyaparGateway CLI (vg listen) — forwards webhooks straight to your localhost without exposing a public URL. Best option when available.
- Deploy-on-PR preview environments — for team workflows, give every PR its own preview URL with its own webhook secret. Removes localhost concerns entirely.
Never reuse production webhook secrets in dev — if a dev tunnel URL leaks, attackers shouldn't be able to forge production-equivalent webhooks.
Debugging webhook failures in production
When a real webhook fails to land or to process, the usual investigation pattern is:
- Check the gateway dashboard's webhook log for the event. Status should show 2xx, retrying, or failed.
- If status is failed, look at the response your endpoint returned. Common: 401 (signature mismatch), 500 (handler crash), timeout (handler too slow).
- Pull the raw payload from the dashboard and replay it against a local instance with verbose logging.
- Check your application's error tracker (Sentry, Rollbar, etc.) for matching stack traces.
- If signature is mismatching, verify you're computing HMAC on the raw body, not a re-serialized JSON, and that you're using the correct secret for the environment.
Operational best practices
Patterns that experienced teams converge on:
- Store the raw webhook body in your DB before doing anything else. Lets you replay or audit later without re-fetching.
- Separate the receive step from the process step — receive the event, ack 200, then process asynchronously via a queue. Keeps handler latency low.
- Alert on the dead-letter queue. If events are landing there, something's broken and a human should know.
- Log the event ID on every line related to a webhook so you can correlate across services.
- Use a separate database table for webhook events (write-only audit log) in addition to updating order state. The audit log is gold during disputes.
With these patterns in place, your webhook handling becomes one of the most boring parts of your stack — which is exactly what payment infrastructure should be. VyaparGateway helps you issue dynamic UPI QR codes, verify payments, and notify your stack via webhooks—without charging a per-transaction platform fee on top of your plan.
Frequently asked questions
- How many times will a webhook be retried if my endpoint is down?
- VyaparGateway retries up to 7 times over a 7-day window with exponential backoff (1m, 5m, 15m, 1h, 6h, 24h, 72h). After 7 days of failed delivery, the event moves to a dead-letter queue you can manually replay from the dashboard. This is more than enough headroom for any reasonable outage.
- What's the best way to test webhooks during local development?
- Use ngrok or Cloudflare Tunnel to expose your local server, or the VyaparGateway CLI's listen command which forwards webhooks directly without a public URL. Always use a separate webhook secret for development so a leaked dev tunnel URL can't be used to forge production-equivalent events.
- Can I replay a webhook I missed?
- Yes. The VyaparGateway dashboard includes a webhook log with a replay button — you can re-send any past event to any of your configured endpoints. This is the safest way to recover after fixing a bug that caused webhooks to fail for a window of time.
- What's the difference between intent.paid and settlement.completed?
- intent.paid fires the moment the customer's payment is detected at your merchant bank or PSP — this is when you should fulfil the order. settlement.completed fires later, when batched settlements actually move from your PSP's account to your final merchant bank account. For direct-to-bank setups the two effectively coincide; for PSP-batched setups, settlement can be hours or a day later.
Free tools for Indian merchants
No sign-up, no ads, no data selling
Use our free, browser-only tools whenever you need them. We don't store the values you enter or track you across the web.
📱 UPI QR Code Generator
Create UPI / URL / WhatsApp / WiFi QRs. Export as SVG or PNG.
🧮 GST Calculator (CGST + SGST + IGST)
Add or remove GST across 5%, 12%, 18%, 28% slabs.