webhook-integration

Dodo Payments Webhook Integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "webhook-integration" with this command: npx skills add dodopayments/skills/dodopayments-skills-webhook-integration

Dodo Payments Webhook Integration

Reference: 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
  • Go to Dashboard → Developer → Webhooks

  • Click "Create Webhook"

  • Enter your endpoint URL

  • Select events to subscribe to

  • Copy the webhook secret

  1. Environment Variables

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

POST /your-webhook-url Content-Type: application/json webhook-id: evt_xxxxx webhook-signature: v1,signature_here webhook-timestamp: 1234567890

Payload Format

{ "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": "customer@example.com", "name": "John Doe" } // ... additional event-specific fields } }

Implementation Examples

Next.js (App Router)

// 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_id, credit_entitlement_name, available_balance, threshold_percent } = data;

// Notify customer about low balance // Suggest upgrading plan or purchasing more credits console.log(Low balance alert: ${available_balance} ${credit_entitlement_name} remaining for ${customer_id}); }

Express.js

import express from 'express'; import crypto from 'crypto';

const app = express(); const WEBHOOK_SECRET = process.env.DODO_PAYMENTS_WEBHOOK_SECRET!;

// Use raw body for signature verification app.post('/webhooks/dodo', express.raw({ type: 'application/json' }), async (req, res) => { const signature = req.headers['webhook-signature'] as string; const timestamp = req.headers['webhook-timestamp'] as string; const payload = req.body.toString();

// Verify signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(signedPayload)
  .digest('base64');

const providedSig = signature?.split(',')[1];

if (!providedSig || !crypto.timingSafeEqual(
  Buffer.from(expectedSig),
  Buffer.from(providedSig)
)) {
  return res.status(401).json({ error: 'Invalid signature' });
}

const event = JSON.parse(payload);

// Process event
try {
  switch (event.type) {
    case 'payment.succeeded':
      await processPayment(event.data);
      break;
    case 'subscription.active':
      await activateSubscription(event.data);
      break;
    // ... handle other events
  }
  res.json({ received: true });
} catch (error) {
  console.error('Webhook processing error:', error);
  res.status(500).json({ error: 'Processing failed' });
}

} );

Python (FastAPI)

from fastapi import FastAPI, Request, HTTPException import hmac import hashlib import base64 import time

app = FastAPI() WEBHOOK_SECRET = os.environ["DODO_PAYMENTS_WEBHOOK_SECRET"]

def verify_signature(payload: bytes, signature: str, timestamp: str) -> bool: signed_payload = f"{timestamp}.{payload.decode()}" expected_sig = base64.b64encode( hmac.new( WEBHOOK_SECRET.encode(), signed_payload.encode(), hashlib.sha256 ).digest() ).decode()

provided_sig = signature.split(',')[1] if ',' in signature else ''
return hmac.compare_digest(expected_sig, provided_sig)

@app.post("/webhooks/dodo") async def handle_webhook(request: Request): body = await request.body() signature = request.headers.get("webhook-signature", "") timestamp = request.headers.get("webhook-timestamp", "")

if not verify_signature(body, signature, timestamp):
    raise HTTPException(status_code=401, detail="Invalid signature")

# Check timestamp freshness
event_time = int(timestamp)
if abs(time.time() - event_time) > 300:
    raise HTTPException(status_code=401, detail="Timestamp too old")

event = json.loads(body)

if event["type"] == "payment.succeeded":
    await handle_payment_succeeded(event["data"])
elif event["type"] == "subscription.active":
    await handle_subscription_active(event["data"])
# ... handle other events

return {"received": True}

Go

package main

import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "io" "net/http" "os" "strconv" "strings" "time" )

var webhookSecret = os.Getenv("DODO_PAYMENTS_WEBHOOK_SECRET")

func verifySignature(payload []byte, signature, timestamp string) bool { signedPayload := timestamp + "." + string(payload)

mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write([]byte(signedPayload))
expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil))

parts := strings.Split(signature, ",")
if len(parts) < 2 {
    return false
}

return hmac.Equal([]byte(expectedSig), []byte(parts[1]))

}

func webhookHandler(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) signature := r.Header.Get("webhook-signature") timestamp := r.Header.Get("webhook-timestamp")

if !verifySignature(body, signature, timestamp) {
    http.Error(w, "Invalid signature", http.StatusUnauthorized)
    return
}

// Check timestamp
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Since(time.Unix(ts, 0)) > 5*time.Minute {
    http.Error(w, "Timestamp too old", http.StatusUnauthorized)
    return
}

var event map[string]interface{}
json.Unmarshal(body, &event)

switch event["type"] {
case "payment.succeeded":
    handlePaymentSucceeded(event["data"].(map[string]interface{}))
case "subscription.active":
    handleSubscriptionActive(event["data"].(map[string]interface{}))
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"received": true})

}

Best Practices

  1. Always Verify Signatures

Never process webhooks without signature verification to prevent spoofing.

  1. Implement Idempotency

Use webhook-id header to prevent duplicate processing:

const processedIds = new Set<string>();

if (processedIds.has(webhookId)) { return NextResponse.json({ received: true }); // Already processed } processedIds.add(webhookId);

  1. Respond Quickly

Return 200 immediately, process asynchronously if needed:

// Queue for async processing await queue.add('process-webhook', event); return NextResponse.json({ received: true });

  1. Handle Retries

Dodo Payments retries failed webhooks. Design handlers to be idempotent.

  1. Log Everything

Keep detailed logs for debugging:

console.log([Webhook] ${event.type} - ${webhookId}, { timestamp: event.timestamp, data: event.data });

Local Development

Using ngrok

Start ngrok tunnel

ngrok http 3000

Use the ngrok URL as your webhook endpoint

https://xxxx.ngrok.io/api/webhooks/dodo

Testing Webhooks

You can trigger test webhooks from the Dodo Payments dashboard:

  • Go to Developer → Webhooks

  • Select your webhook

  • Click "Send Test Event"

Troubleshooting

Signature Verification Failing

  • Ensure you're using the raw request body

  • Check webhook secret is correct

  • Verify timestamp format (Unix seconds)

Not Receiving Webhooks

  • Check endpoint is publicly accessible

  • Verify webhook is enabled in dashboard

  • Check server logs for errors

Duplicate Events

  • Implement idempotency using webhook-id

  • Store processed event IDs in database

Resources

  • Webhook Documentation

  • Event Reference

  • Standard Webhooks Spec

  • Credit Webhook Events

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dodo-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

checkout-integration

No summary provided by upstream source.

Repository SourceNeeds Review
General

subscription-integration

No summary provided by upstream source.

Repository SourceNeeds Review