recur-webhooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRecur Webhook Integration
Recur Webhook 集成
You are helping implement Recur webhooks to receive real-time payment and subscription events.
你将协助实现Recur webhooks,以接收实时支付和订阅事件。
Webhook Events
Webhook 事件
Core Events (Most Common)
核心事件(最常用)
| Event | When Fired |
|---|---|
| Payment successful, subscription/order created |
| Subscription is now active |
| Subscription was cancelled |
| Recurring payment successful |
| Payment failed, subscription at risk |
| One-time purchase completed |
| Refund initiated |
| 事件 | 触发时机 |
|---|---|
| 支付成功,订阅/订单已创建 |
| 订阅现已激活 |
| 订阅已取消 |
| 定期支付成功 |
| 支付失败,订阅存在风险 |
| 一次性购买完成 |
| 退款已发起 |
All Supported Events
所有支持的事件
typescript
type WebhookEventType =
// Checkout
| 'checkout.created'
| 'checkout.completed'
// Orders
| 'order.paid'
| 'order.payment_failed'
// Subscription Lifecycle
| 'subscription.created'
| 'subscription.activated'
| 'subscription.cancelled'
| 'subscription.expired'
| 'subscription.trial_ending'
// Subscription Changes
| 'subscription.upgraded'
| 'subscription.downgraded'
| 'subscription.renewed'
| 'subscription.past_due'
// Scheduled Changes
| 'subscription.schedule_created'
| 'subscription.schedule_executed'
| 'subscription.schedule_cancelled'
// Invoices
| 'invoice.created'
| 'invoice.paid'
| 'invoice.payment_failed'
// Customer
| 'customer.created'
| 'customer.updated'
// Product
| 'product.created'
| 'product.updated'
// Refunds
| 'refund.created'
| 'refund.succeeded'
| 'refund.failed'typescript
type WebhookEventType =
// Checkout
| 'checkout.created'
| 'checkout.completed'
// Orders
| 'order.paid'
| 'order.payment_failed'
// Subscription Lifecycle
| 'subscription.created'
| 'subscription.activated'
| 'subscription.cancelled'
| 'subscription.expired'
| 'subscription.trial_ending'
// Subscription Changes
| 'subscription.upgraded'
| 'subscription.downgraded'
| 'subscription.renewed'
| 'subscription.past_due'
// Scheduled Changes
| 'subscription.schedule_created'
| 'subscription.schedule_executed'
| 'subscription.schedule_cancelled'
// Invoices
| 'invoice.created'
| 'invoice.paid'
| 'invoice.payment_failed'
// Customer
| 'customer.created'
| 'customer.updated'
// Product
| 'product.created'
| 'product.updated'
// Refunds
| 'refund.created'
| 'refund.succeeded'
| 'refund.failed'Webhook Handler Implementation
Webhook 处理器实现
Next.js App Router
Next.js App 路由
typescript
// app/api/webhooks/recur/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
const WEBHOOK_SECRET = process.env.RECUR_WEBHOOK_SECRET!
function verifySignature(payload: string, signature: string): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
export async function POST(request: NextRequest) {
const payload = await request.text()
const signature = request.headers.get('x-recur-signature')
// Verify signature
if (!signature || !verifySignature(payload, signature)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
const event = JSON.parse(payload)
// Handle events
switch (event.type) {
case 'checkout.completed':
await handleCheckoutCompleted(event.data)
break
case 'subscription.activated':
await handleSubscriptionActivated(event.data)
break
case 'subscription.cancelled':
await handleSubscriptionCancelled(event.data)
break
case 'subscription.renewed':
await handleSubscriptionRenewed(event.data)
break
case 'subscription.past_due':
await handleSubscriptionPastDue(event.data)
break
case 'refund.created':
await handleRefundCreated(event.data)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}
// Event handlers
async function handleCheckoutCompleted(data: any) {
const { customerId, subscriptionId, orderId, productId, amount } = data
// Update your database
// Grant access to the user
// Send confirmation email
}
async function handleSubscriptionActivated(data: any) {
const { subscriptionId, customerId, productId, status } = data
// Update user's subscription status in your database
// Enable premium features
}
async function handleSubscriptionCancelled(data: any) {
const { subscriptionId, customerId, cancelledAt, accessUntil } = data
// Mark subscription as cancelled
// User still has access until accessUntil date
// Send cancellation confirmation email
}
async function handleSubscriptionRenewed(data: any) {
const { subscriptionId, customerId, amount, nextBillingDate } = data
// Update billing records
// Extend access period
}
async function handleSubscriptionPastDue(data: any) {
const { subscriptionId, customerId, failureReason } = data
// Notify user of payment failure
// Consider sending dunning emails
// May want to restrict access after grace period
}
async function handleRefundCreated(data: any) {
const { refundId, orderId, amount, reason } = data
// Update order status
// Adjust user credits/access
// Send refund notification
}typescript
// app/api/webhooks/recur/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
const WEBHOOK_SECRET = process.env.RECUR_WEBHOOK_SECRET!
function verifySignature(payload: string, signature: string): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
export async function POST(request: NextRequest) {
const payload = await request.text()
const signature = request.headers.get('x-recur-signature')
// 验证签名
if (!signature || !verifySignature(payload, signature)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
const event = JSON.parse(payload)
// 处理事件
switch (event.type) {
case 'checkout.completed':
await handleCheckoutCompleted(event.data)
break
case 'subscription.activated':
await handleSubscriptionActivated(event.data)
break
case 'subscription.cancelled':
await handleSubscriptionCancelled(event.data)
break
case 'subscription.renewed':
await handleSubscriptionRenewed(event.data)
break
case 'subscription.past_due':
await handleSubscriptionPastDue(event.data)
break
case 'refund.created':
await handleRefundCreated(event.data)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}
// 事件处理器
async function handleCheckoutCompleted(data: any) {
const { customerId, subscriptionId, orderId, productId, amount } = data
// 更新你的数据库
// 为用户开通权限
// 发送确认邮件
}
async function handleSubscriptionActivated(data: any) {
const { subscriptionId, customerId, productId, status } = data
// 在数据库中更新用户的订阅状态
// 启用高级功能
}
async function handleSubscriptionCancelled(data: any) {
const { subscriptionId, customerId, cancelledAt, accessUntil } = data
// 将订阅标记为已取消
// 用户在accessUntil日期前仍可访问
// 发送取消确认邮件
}
async function handleSubscriptionRenewed(data: any) {
const { subscriptionId, customerId, amount, nextBillingDate } = data
// 更新账单记录
// 延长访问期限
}
async function handleSubscriptionPastDue(data: any) {
const { subscriptionId, customerId, failureReason } = data
// 通知用户支付失败
// 考虑发送催缴邮件
// 宽限期后可限制访问
}
async function handleRefundCreated(data: any) {
const { refundId, orderId, amount, reason } = data
// 更新订单状态
// 调整用户额度/访问权限
// 发送退款通知
}Express.js
Express.js
typescript
import express from 'express'
import crypto from 'crypto'
const app = express()
// Important: Use raw body for signature verification
app.post(
'/api/webhooks/recur',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString()
const signature = req.headers['x-recur-signature'] as string
// Verify signature
const expected = crypto
.createHmac('sha256', process.env.RECUR_WEBHOOK_SECRET!)
.update(payload)
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' })
}
const event = JSON.parse(payload)
// Handle event...
console.log('Received event:', event.type)
res.json({ received: true })
}
)typescript
import express from 'express'
import crypto from 'crypto'
const app = express()
// 重要:使用原始请求体进行签名验证
app.post(
'/api/webhooks/recur',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString()
const signature = req.headers['x-recur-signature'] as string
// 验证签名
const expected = crypto
.createHmac('sha256', process.env.RECUR_WEBHOOK_SECRET!)
.update(payload)
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' })
}
const event = JSON.parse(payload)
// 处理事件...
console.log('Received event:', event.type)
res.json({ received: true })
}
)Event Payload Structure
事件负载结构
typescript
interface WebhookEvent {
id: string // Event ID (for idempotency)
type: string // Event type
timestamp: string // ISO 8601 timestamp
data: {
// Varies by event type
customerId?: string
customerEmail?: string
subscriptionId?: string
orderId?: string
productId?: string
amount?: number
currency?: string
// ... more fields depending on event
}
}typescript
interface WebhookEvent {
id: string // 事件ID(用于幂等性)
type: string // 事件类型
timestamp: string // ISO 8601 时间戳
data: {
// 随事件类型变化
customerId?: string
customerEmail?: string
subscriptionId?: string
orderId?: string
productId?: string
amount?: number
currency?: string
// ... 更多字段取决于事件类型
}
}Webhook Configuration
Webhook 配置
- Go to Recur Dashboard → Settings → Webhooks
- Click Add Endpoint
- Enter your endpoint URL (e.g., )
https://yourapp.com/api/webhooks/recur - Select events to receive
- Copy the Webhook Secret to your environment variables
- 前往 Recur 控制台 → 设置 → Webhooks
- 点击 添加端点
- 输入你的端点URL(例如:)
https://yourapp.com/api/webhooks/recur - 选择要接收的事件
- 将 Webhook 密钥复制到你的环境变量中
Testing Webhooks Locally
本地测试Webhooks
Using ngrok
使用ngrok
bash
undefinedbash
undefinedStart ngrok tunnel
启动ngrok隧道
ngrok http 3000
ngrok http 3000
Use the ngrok URL in Recur dashboard
在Recur控制台中使用ngrok URL
undefinedundefinedUsing Recur CLI (if available)
使用Recur CLI(如果可用)
bash
undefinedbash
undefinedForward webhooks to local server
将webhooks转发到本地服务器
recur webhooks forward --to localhost:3000/api/webhooks/recur
undefinedrecur webhooks forward --to localhost:3000/api/webhooks/recur
undefinedBest Practices
最佳实践
1. Always Verify Signatures
1. 始终验证签名
Never trust webhook payloads without verifying the signature.
在未验证签名的情况下,绝不要信任webhook负载。
2. Handle Idempotency
2. 处理幂等性
Webhooks may be delivered multiple times. Use the event to deduplicate:
idtypescript
async function handleEvent(event: WebhookEvent) {
// Check if already processed
const existing = await db.webhookEvent.findUnique({
where: { eventId: event.id }
})
if (existing) {
console.log('Event already processed:', event.id)
return
}
// Process event...
// Mark as processed
await db.webhookEvent.create({
data: { eventId: event.id, processedAt: new Date() }
})
}Webhook可能会多次投递。使用事件来避免重复处理:
idtypescript
async function handleEvent(event: WebhookEvent) {
// 检查是否已处理过
const existing = await db.webhookEvent.findUnique({
where: { eventId: event.id }
})
if (existing) {
console.log('Event already processed:', event.id)
return
}
// 处理事件...
// 标记为已处理
await db.webhookEvent.create({
data: { eventId: event.id, processedAt: new Date() }
})
}3. Return 200 Quickly
3. 快速返回200响应
Process events asynchronously to avoid timeouts:
typescript
export async function POST(request: NextRequest) {
// Verify and parse...
// Queue for async processing
await queue.add('process-webhook', event)
// Return immediately
return NextResponse.json({ received: true })
}异步处理事件以避免超时:
typescript
export async function POST(request: NextRequest) {
// 验证并解析...
// 加入队列进行异步处理
await queue.add('process-webhook', event)
// 立即返回响应
return NextResponse.json({ received: true })
}4. Handle Retries Gracefully
4. 优雅处理重试
Recur retries failed webhook deliveries. Ensure your handler is idempotent.
Recur会重试失败的webhook投递。确保你的处理器是幂等的。
5. Log Everything
5. 记录所有操作
typescript
console.log('Webhook received:', {
type: event.type,
id: event.id,
timestamp: event.timestamp,
})typescript
console.log('Webhook received:', {
type: event.type,
id: event.id,
timestamp: event.timestamp,
})Debugging Webhooks
调试Webhooks
Check Webhook Logs
查看Webhook日志
In Recur Dashboard → Webhooks → Click endpoint → View delivery logs
在Recur控制台 → Webhooks → 点击端点 → 查看投递日志
Common Issues
常见问题
401 Unauthorized
- Check webhook secret is correct
- Ensure using raw body for signature verification
- Verify signature algorithm (HMAC SHA-256)
Timeout (no response)
- Return 200 before processing
- Use async processing for heavy operations
Missing events
- Check event types are selected in dashboard
- Verify endpoint URL is correct and accessible
401 Unauthorized(未授权)
- 检查webhook密钥是否正确
- 确保使用原始请求体进行签名验证
- 验证签名算法(HMAC SHA-256)
Timeout(无响应超时)
- 在处理前返回200响应
- 对耗时操作使用异步处理
事件缺失
- 检查控制台中是否已选择对应的事件类型
- 验证端点URL是否正确且可访问
Related Skills
相关技能
- - Initial SDK setup
/recur-quickstart - - Implement payment flows
/recur-checkout - - Check subscription access after webhook
/recur-entitlements
- - 初始SDK设置
/recur-quickstart - - 实现支付流程
/recur-checkout - - Webhook后检查订阅权限
/recur-entitlements