Claude
Skills
Sign in
Back

webhook-integration

Included with Lifetime
$97 forever

Complete guide for setting up and handling Dodo Payments webhooks for real-time payment event notifications.

Sales & CRM

What this skill does


# Dodo Payments Webhook Integration

**Reference: [docs.dodopayments.com/developer-resources/webhooks](https://docs.dodopayments.com/developer-resources/webhooks)**

Webhooks provide real-time notifications when payment events occur. Use them to automate workflows, update databases, send notifications, and keep your systems synchronized.

---

## Quick Setup

### 1. Configure Webhook in Dashboard
1. Go to Dashboard → Developer → Webhooks
2. Click "Create Webhook"
3. Enter your endpoint URL
4. Select events to subscribe to
5. Copy the webhook secret

### 2. Environment Variables
```bash
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
```

---

## Webhook Events

### Payment Events
| Event | Description |
|-------|-------------|
| `payment.succeeded` | Payment completed successfully |
| `payment.failed` | Payment attempt failed |
| `payment.processing` | Payment is being processed |
| `payment.cancelled` | Payment was cancelled |

### Subscription Events
| Event | Description |
|-------|-------------|
| `subscription.active` | Subscription is now active |
| `subscription.updated` | Subscription details changed |
| `subscription.on_hold` | Subscription on hold (failed renewal) |
| `subscription.renewed` | Subscription renewed successfully |
| `subscription.plan_changed` | Plan upgraded/downgraded |
| `subscription.cancelled` | Subscription cancelled |
| `subscription.failed` | Subscription creation failed |
| `subscription.expired` | Subscription term ended |

### Other Events
| Event | Description |
|-------|-------------|
| `refund.succeeded` | Refund processed successfully |
| `dispute.opened` | New dispute received |
| `license_key.created` | License key generated |

### Credit Events
| Event | Description |
|-------|-------------|
| `credit.added` | Credits granted to a customer (subscription, one-time, or API) |
| `credit.deducted` | Credits consumed through usage or manual debit |
| `credit.expired` | Unused credits expired after configured period |
| `credit.rolled_over` | Unused credits carried forward at cycle end |
| `credit.rollover_forfeited` | Credits forfeited at max rollover count |
| `credit.overage_charged` | Overage charges applied beyond zero balance |
| `credit.manual_adjustment` | Manual credit/debit adjustment via dashboard or API |
| `credit.balance_low` | Credit balance dropped below configured threshold |
---

## Webhook Payload Structure

### Request Headers
```http
POST /your-webhook-url
Content-Type: application/json
webhook-id: evt_xxxxx
webhook-signature: v1,signature_here
webhook-timestamp: 1234567890
```

### Payload Format
```json
{
  "business_id": "bus_xxxxx",
  "type": "payment.succeeded",
  "timestamp": "2024-01-01T12:00:00Z",
  "data": {
    "payload_type": "Payment",
    "payment_id": "pay_xxxxx",
    "total_amount": 2999,
    "currency": "USD",
    "customer": {
      "customer_id": "cust_xxxxx",
      "email": "[email protected]",
      "name": "John Doe"
    }
    // ... additional event-specific fields
  }
}
```

---

## Implementation Examples

### Next.js (App Router)

```typescript
// app/api/webhooks/dodo/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.DODO_PAYMENTS_WEBHOOK_SECRET!;

function verifySignature(payload: string, signature: string, timestamp: string): boolean {
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('base64');
  
  // Extract signature from "v1,signature" format
  const providedSig = signature.split(',')[1];
  
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(providedSig || '')
  );
}

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get('webhook-signature') || '';
  const timestamp = req.headers.get('webhook-timestamp') || '';
  const webhookId = req.headers.get('webhook-id');

  // Verify signature
  if (!verifySignature(body, signature, timestamp)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  // Check timestamp to prevent replay attacks (5 minute tolerance)
  const eventTime = parseInt(timestamp) * 1000;
  if (Math.abs(Date.now() - eventTime) > 300000) {
    return NextResponse.json({ error: 'Timestamp too old' }, { status: 401 });
  }

  const event = JSON.parse(body);

  // Handle events
  switch (event.type) {
    case 'payment.succeeded':
      await handlePaymentSucceeded(event.data);
      break;
    case 'payment.failed':
      await handlePaymentFailed(event.data);
      break;
    case 'subscription.active':
      await handleSubscriptionActive(event.data);
      break;
    case 'subscription.cancelled':
      await handleSubscriptionCancelled(event.data);
      break;
    case 'refund.succeeded':
      await handleRefundSucceeded(event.data);
      break;
    case 'dispute.opened':
      await handleDisputeOpened(event.data);
      break;
    case 'license_key.created':
      await handleLicenseKeyCreated(event.data);
      break;
    case 'credit.added':
      await handleCreditAdded(event.data);
      break;
    case 'credit.deducted':
      await handleCreditDeducted(event.data);
      break;
    case 'credit.balance_low':
      await handleCreditBalanceLow(event.data);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return NextResponse.json({ received: true });
}

async function handlePaymentSucceeded(data: any) {
  const { payment_id, customer, total_amount, product_id, subscription_id } = data;
  
  // Update database
  // Send confirmation email
  // Grant access to product
  console.log(`Payment ${payment_id} succeeded for ${customer.email}`);
}

async function handlePaymentFailed(data: any) {
  const { payment_id, customer, error_message } = data;
  
  // Log failure
  // Notify customer
  // Update UI state
  console.log(`Payment ${payment_id} failed: ${error_message}`);
}

async function handleSubscriptionActive(data: any) {
  const { subscription_id, customer, product_id, next_billing_date } = data;
  
  // Grant subscription access
  // Update user record
  // Send welcome email
  console.log(`Subscription ${subscription_id} activated for ${customer.email}`);
}

async function handleSubscriptionCancelled(data: any) {
  const { subscription_id, customer, cancelled_at, cancel_at_next_billing_date } = data;
  
  // Schedule access revocation
  // Send cancellation confirmation
  console.log(`Subscription ${subscription_id} cancelled`);
}

async function handleRefundSucceeded(data: any) {
  const { refund_id, payment_id, amount } = data;
  
  // Update order status
  // Revoke access if needed
  console.log(`Refund ${refund_id} processed for payment ${payment_id}`);
}

async function handleDisputeOpened(data: any) {
  const { dispute_id, payment_id, amount, dispute_status } = data;
  
  // Alert team
  // Prepare evidence
  console.log(`Dispute ${dispute_id} opened for payment ${payment_id}`);
}

async function handleLicenseKeyCreated(data: any) {
  const { id, key, product_id, customer_id, expires_at } = data;
  
  // Store license key
  // Send to customer
  console.log(`License key created: ${key.substring(0, 8)}...`);
}

async function handleCreditAdded(data: any) {
  const { customer_id, credit_entitlement_id, amount, balance_after } = data;
  
  // Update internal credit balance
  // Log credit grant
  console.log(`${amount} credits added for customer ${customer_id}, balance: ${balance_after}`);
}

async function handleCreditDeducted(data: any) {
  const { customer_id, credit_entitlement_id, amount, balance_after } = data;
  
  // Update internal credit balance
  // Check if balance is getting low
  console.log(`${amount} credits deducted for customer ${customer_id}, balance: ${balance_after}`);
}

async function handleCreditBalanceLow(data: any) {
  const { customer

Related in Sales & CRM