# How to Set Up Webhooks for Payment Notifications

> Learn how webhooks work for payment notifications, how to set up endpoints, verify signatures, and handle payment events reliably in your SaaS application.
- **Author**: Ayush Agarwal
- **Published**: 2026-04-13
- **Category**: Developer Guide
- **URL**: https://dodopayments.com/blogs/webhooks-payment-notifications

---

Polling an API every few seconds to check if a payment succeeded is wasteful and unreliable. Webhooks solve this by pushing payment events to your server the moment they happen. A customer completes checkout, their subscription renews, a payment fails, a refund processes - your application knows immediately without asking.

For SaaS businesses, webhooks are the backbone of post-payment logic. They trigger license key delivery, activate subscriptions, update user permissions, send confirmation emails, and sync billing data with your database. Without webhooks, your payment integration is flying blind between API calls.

This guide covers how payment webhooks work, how to set them up properly, and the mistakes that cause missed events and broken payment flows.

## How Payment Webhooks Work

A webhook is an HTTP POST request sent from a payment provider to your server when a specific event occurs. Unlike APIs where you pull data, webhooks push data to you.

```mermaid
flowchart LR
    A[Customer Pays] -->|"Event created"| B[Payment Provider]
    B -->|"HTTP POST"| C[Your Webhook Endpoint]
    C -->|"Process event"| D[Your Application Logic]
    D -->|"Update DB"| E[Activate Subscription]
    D -->|"Send email"| F[Confirmation to Customer]
    C -->|"Return 200 OK"| B
```

The flow is straightforward:

1. An event occurs (payment succeeds, fails, refunds, etc.)
2. The payment provider creates an event payload with all relevant data
3. The provider sends an HTTP POST request to your registered webhook URL
4. Your server processes the event and takes action
5. Your server responds with a 200 status code to acknowledge receipt
6. If your server does not respond with 200, the provider retries

## Common Payment Webhook Events

Most payment providers send webhooks for these core events:

| Event                    | When It Fires             | What You Should Do                                                                    |
| ------------------------ | ------------------------- | ------------------------------------------------------------------------------------- |
| `payment.succeeded`      | Charge completed          | Activate access, send receipt                                                         |
| `payment.failed`         | Charge declined           | Notify customer, trigger [dunning](https://dodopayments.com/blogs/dunning-management) |
| `subscription.created`   | New subscription starts   | Provision account, set plan                                                           |
| `subscription.renewed`   | Recurring charge succeeds | Extend access period                                                                  |
| `subscription.cancelled` | Customer cancels          | Schedule access revocation                                                            |
| `refund.created`         | Refund processed          | Revoke access, update records                                                         |
| `dispute.created`        | Chargeback filed          | Flag account, gather evidence                                                         |

With [Dodo Payments webhooks](https://docs.dodopayments.com/developer-resources/webhooks), you receive events for the full payment lifecycle including one-time payments, subscriptions, refunds, and disputes.

## Setting Up a Webhook Endpoint

### Step 1: Create the Endpoint

Your webhook endpoint is a standard HTTP route that accepts POST requests. Here is a basic example using Node.js and Express:

```javascript
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

app.post("/webhooks/payments", async (req, res) => {
  const event = req.body;

  switch (event.type) {
    case "payment.succeeded":
      // Activate the customer's subscription or deliver the product
      await activateSubscription(event.data.customer_id, event.data.product_id);
      break;
    case "payment.failed":
      // Notify the customer about the failed payment
      await sendFailedPaymentEmail(event.data.customer_email);
      break;
    case "refund.created":
      // Revoke access and update internal records
      await revokeAccess(event.data.customer_id);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  res.status(200).json({ received: true });
});

app.listen(3000);
```

Key requirements for the endpoint:

- Must be publicly accessible (not localhost)
- Must accept POST requests with JSON body
- Must respond with 200 status within 5-30 seconds
- Should be idempotent (handling the same event twice produces the same result)

### Step 2: Register the URL

In your payment provider's dashboard or via [API](https://docs.dodopayments.com/api-reference/introduction), register your webhook URL and select which events you want to receive. Start with the events you actually need rather than subscribing to everything.

With Dodo Payments, configure webhook endpoints through the dashboard or use the [integration guide](https://docs.dodopayments.com/developer-resources/integration-guide) for programmatic setup.

### Step 3: Verify Webhook Signatures

Never trust incoming webhook data without verification. Payment providers sign each webhook with a secret key so you can confirm it actually came from them and was not tampered with.

```javascript
function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(payload))
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature),
  );
}

app.post("/webhooks/payments", (req, res) => {
  const signature = req.headers["x-webhook-signature"];

  if (
    !verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)
  ) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // Process verified event...
  res.status(200).json({ received: true });
});
```

> Webhook signature verification is not optional. Without it, anyone who discovers your webhook URL can send fake events to your server, potentially granting unauthorized access or triggering false refunds.
>
> - Ayush Agarwal, Co-founder & CPTO at Dodo Payments

### Step 4: Handle Retries and Idempotency

Webhook delivery is at-least-once, meaning you might receive the same event multiple times. Your handler must be idempotent:

```javascript
async function handlePaymentSucceeded(event) {
  // Check if we already processed this event
  const existing = await db.webhookEvents.findOne({ eventId: event.id });
  if (existing) {
    return; // Already processed, skip
  }

  // Process the event
  await activateSubscription(event.data.customer_id);

  // Record that we processed it
  await db.webhookEvents.create({ eventId: event.id, processedAt: new Date() });
}
```

Store processed event IDs and check before processing. This prevents double-activating subscriptions, sending duplicate emails, or creating duplicate records.

## Common Webhook Mistakes

### Doing Too Much Work Synchronously

If your webhook handler takes longer than the provider's timeout (usually 5-30 seconds), the delivery is marked as failed and retried. This creates duplicate processing.

**Fix**: Acknowledge the webhook immediately, then process asynchronously:

```javascript
app.post("/webhooks/payments", (req, res) => {
  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookEvent(req.body).catch(console.error);
});
```

### Not Handling Event Ordering

Events can arrive out of order. A `subscription.cancelled` event might arrive before `subscription.created` if there is a network delay. Design your handlers to be order-independent or queue events and process them sequentially.

### Exposing Webhook Endpoints Without Authentication

Without signature verification, your webhook endpoint is an open door. An attacker could send crafted events to grant themselves access, trigger refunds, or corrupt your data. Always verify signatures before processing.

### Ignoring Failed Deliveries

Most providers have a retry policy with exponential backoff. If your endpoint is down for an extended period, you might miss events permanently. Monitor webhook delivery in your provider's dashboard and set up alerts for failures.

## Testing Webhooks Locally

During development, your localhost is not accessible from the internet. Use tunneling tools to expose your local server:

- **ngrok**: Creates a public URL that tunnels to localhost
- **Cloudflare Tunnel**: Similar to ngrok with Cloudflare's network
- **localtunnel**: Open source alternative

```bash
# Using ngrok
ngrok http 3000
# Gives you a public URL like abc123.ngrok.io
# Register the generated URL + /webhooks/payments as your webhook URL
```

You can also use the Dodo Payments [SDK](https://docs.dodopayments.com/developer-resources/dodo-payments-sdks) test mode to send simulated events to your endpoint during development.

## Webhook Architecture for Production

For production systems processing significant volume, consider this architecture:

```mermaid
flowchart LR
    A[Payment Provider] -->|"Webhook"| B[API Gateway]
    B -->|"Verify + Queue"| C[Message Queue]
    C -->|"Consume"| D[Worker Process]
    D -->|"Process"| E[Database]
    D -->|"Notify"| F[Email Service]
    D -->|"Update"| G[Subscription Service]
```

Benefits of queue-based processing:

- **Reliability**: Events persist in the queue even if workers crash
- **Scalability**: Add more workers to handle spikes
- **Retry logic**: Built-in retry with dead letter queues
- **Ordering**: Process events sequentially per customer

## Webhook Security Checklist

- [ ] Verify webhook signatures on every request
- [ ] Use HTTPS for your webhook endpoint
- [ ] Implement idempotency using event IDs
- [ ] Set a short timeout and process asynchronously
- [ ] Log all received events for debugging
- [ ] Monitor delivery success rates
- [ ] Handle retries gracefully
- [ ] Reject events with invalid or missing signatures
- [ ] Keep your webhook secret secure (environment variables, not code)
- [ ] Test with simulated events before going live

## Integrating with Dodo Payments Webhooks

[Dodo Payments](https://dodopayments.com) provides webhooks for all payment lifecycle events. The [webhook documentation](https://docs.dodopayments.com/developer-resources/webhooks) covers:

- Event types and payload schemas
- Signature verification methods
- Retry policies and timeout configuration
- Testing with sandbox events

Combined with the [Dodo API](https://docs.dodopayments.com/api-reference/introduction) and [SDKs](https://docs.dodopayments.com/developer-resources/dodo-payments-sdks), webhooks give you complete visibility into payment state changes across one-time payments, [subscriptions](https://docs.dodopayments.com/features/subscription), and [license key](https://docs.dodopayments.com/features/license-keys) activations.

For a complete integration walkthrough, see the [developer experience guide](https://dodopayments.com/blogs/developer-experience-dodo-payments) and [best billing APIs for developers](https://dodopayments.com/blogs/best-billing-apis-developers).

## FAQ

### What happens if my webhook endpoint is down?

Payment providers retry failed webhook deliveries with exponential backoff, typically for 24-72 hours. If your endpoint remains unreachable, the events are eventually dropped. Most providers show failed deliveries in their dashboard so you can manually replay them. Design your system to reconcile state via API polling as a fallback.

### How do I test webhooks during local development?

Use a tunneling service like ngrok or Cloudflare Tunnel to expose your local server to the internet. Start ngrok with `ngrok http 3000`, then register the generated public URL as your webhook endpoint in your payment provider's dashboard. Most providers also offer test event simulators.

### Should I process webhooks synchronously or asynchronously?

Always acknowledge the webhook immediately with a 200 response, then process asynchronously using a message queue or background job. Synchronous processing risks timeouts, which trigger retries and can cause duplicate processing. Queue-based architectures provide reliability, scalability, and built-in retry logic.

### How do I prevent duplicate webhook processing?

Implement idempotency by storing processed event IDs in your database. Before processing any event, check if you have already handled an event with the same ID. If yes, skip it. This ensures that retried deliveries do not cause duplicate actions like double-charging or sending multiple confirmation emails.

### What is webhook signature verification and why is it important?

Webhook signature verification confirms that an incoming webhook actually came from your payment provider and was not tampered with in transit. The provider signs each request using a shared secret. Your server recomputes the signature and compares it. Without verification, anyone who discovers your webhook URL could send malicious fake events.

## Final Thoughts

Webhooks are the event-driven backbone of any payment integration. Set them up correctly from the start - verify signatures, implement idempotency, process asynchronously - and you will have a reliable system that scales.

For a payment platform with well-documented webhooks and developer-friendly SDKs, explore [Dodo Payments](https://dodopayments.com) and check the [pricing](https://dodopayments.com/pricing).
---
- [More Developer Guide articles](https://dodopayments.com/blogs/category/developer-guide)
- [All articles](https://dodopayments.com/blogs)