webhook-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDodo Payments Webhook Integration
Dodo Payments Webhook 集成
Webhooks provide real-time notifications when payment events occur. Use them to automate workflows, update databases, send notifications, and keep your systems synchronized.
Webhook 可在支付事件发生时提供实时通知。您可以使用它们来自动化工作流、更新数据库、发送通知并保持系统同步。
Quick Setup
快速设置
1. Configure Webhook in Dashboard
1. 在控制台配置 Webhook
- Go to Dashboard → Developer → Webhooks
- Click "Create Webhook"
- Enter your endpoint URL
- Select events to subscribe to
- Copy the webhook secret
- 进入控制台 → 开发者 → Webhook
- 点击“创建 Webhook”
- 输入您的端点 URL
- 选择要订阅的事件
- 复制 Webhook 密钥
2. Environment Variables
2. 环境变量
bash
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_herebash
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_hereWebhook Events
Webhook 事件
Payment Events
支付事件
| Event | Description |
|---|---|
| Payment completed successfully |
| Payment attempt failed |
| Payment is being processed |
| Payment was cancelled |
| 事件 | 描述 |
|---|---|
| 支付成功完成 |
| 支付尝试失败 |
| 支付处理中 |
| 支付已取消 |
Subscription Events
订阅事件
| Event | Description |
|---|---|
| Subscription is now active |
| Subscription details changed |
| Subscription on hold (failed renewal) |
| Subscription renewed successfully |
| Plan upgraded/downgraded |
| Subscription cancelled |
| Subscription creation failed |
| Subscription term ended |
| 事件 | 描述 |
|---|---|
| 订阅已激活 |
| 订阅详情已更改 |
| 订阅暂停(续费失败) |
| 订阅续费成功 |
| 套餐已升级/降级 |
| 订阅已取消 |
| 订阅创建失败 |
| 订阅期限已结束 |
Other Events
其他事件
| Event | Description |
|---|---|
| Refund processed successfully |
| New dispute received |
| License key generated |
| 事件 | 描述 |
|---|---|
| 退款处理成功 |
| 收到新的争议 |
| 生成许可证密钥 |
Webhook Payload Structure
Webhook 负载结构
Request Headers
请求头
http
POST /your-webhook-url
Content-Type: application/json
webhook-id: evt_xxxxx
webhook-signature: v1,signature_here
webhook-timestamp: 1234567890http
POST /your-webhook-url
Content-Type: application/json
webhook-id: evt_xxxxx
webhook-signature: v1,signature_here
webhook-timestamp: 1234567890Payload 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": "customer@example.com",
"name": "John Doe"
}
// ... additional event-specific fields
}
}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": "customer@example.com",
"name": "John Doe"
}
// ... 其他事件特定字段
}
}Implementation Examples
实现示例
Next.js (App Router)
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;
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)}...`);
}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;
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)}...`);
}Express.js
Express.js
typescript
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' });
}
}
);typescript
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)
Python(FastAPI)
python
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}python
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import base64
import time
import os
import json
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
Go
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})
}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
1. 始终验证签名
Never process webhooks without signature verification to prevent spoofing.
切勿在未验证签名的情况下处理Webhook,以防止伪造请求。
2. Implement Idempotency
2. 实现幂等性
Use header to prevent duplicate processing:
webhook-idtypescript
const processedIds = new Set<string>();
if (processedIds.has(webhookId)) {
return NextResponse.json({ received: true }); // Already processed
}
processedIds.add(webhookId);使用标头防止重复处理:
webhook-idtypescript
const processedIds = new Set<string>();
if (processedIds.has(webhookId)) {
return NextResponse.json({ received: true }); // 已处理
}
processedIds.add(webhookId);3. Respond Quickly
3. 快速响应
Return 200 immediately, process asynchronously if needed:
typescript
// Queue for async processing
await queue.add('process-webhook', event);
return NextResponse.json({ received: true });立即返回200状态码,如有需要异步处理:
typescript
// 加入队列异步处理
await queue.add('process-webhook', event);
return NextResponse.json({ received: true });4. Handle Retries
4. 处理重试
Dodo Payments retries failed webhooks. Design handlers to be idempotent.
Dodo Payments会重试失败的Webhook。设计处理程序以支持幂等性。
5. Log Everything
5. 记录所有操作
Keep detailed logs for debugging:
typescript
console.log(`[Webhook] ${event.type} - ${webhookId}`, {
timestamp: event.timestamp,
data: event.data
});保留详细日志以便调试:
typescript
console.log(`[Webhook] ${event.type} - ${webhookId}`, {
timestamp: event.timestamp,
data: event.data
});Local Development
本地开发
Using ngrok
使用ngrok
bash
undefinedbash
undefinedStart ngrok tunnel
启动ngrok隧道
ngrok http 3000
ngrok http 3000
Use the ngrok URL as your webhook endpoint
使用ngrok URL作为Webhook端点
undefinedundefinedTesting Webhooks
测试Webhook
You can trigger test webhooks from the Dodo Payments dashboard:
- Go to Developer → Webhooks
- Select your webhook
- Click "Send Test Event"
您可以从Dodo Payments控制台触发测试Webhook:
- 进入开发者 → Webhook
- 选择您的Webhook
- 点击“发送测试事件”
Troubleshooting
故障排除
Signature Verification Failing
签名验证失败
- Ensure you're using the raw request body
- Check webhook secret is correct
- Verify timestamp format (Unix seconds)
- 确保您使用的是原始请求体
- 检查Webhook密钥是否正确
- 验证时间戳格式(Unix秒)
Not Receiving Webhooks
未收到Webhook
- Check endpoint is publicly accessible
- Verify webhook is enabled in dashboard
- Check server logs for errors
- 检查端点是否可公开访问
- 验证控制台中Webhook是否已启用
- 检查服务器日志是否有错误
Duplicate Events
重复事件
- Implement idempotency using webhook-id
- Store processed event IDs in database
- 使用webhook-id实现幂等性
- 在数据库中存储已处理的事件ID