recur-webhooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Recur 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)

核心事件(最常用)

EventWhen Fired
checkout.completed
Payment successful, subscription/order created
subscription.activated
Subscription is now active
subscription.cancelled
Subscription was cancelled
subscription.renewed
Recurring payment successful
subscription.past_due
Payment failed, subscription at risk
order.paid
One-time purchase completed
refund.created
Refund initiated
事件触发时机
checkout.completed
支付成功,订阅/订单已创建
subscription.activated
订阅现已激活
subscription.cancelled
订阅已取消
subscription.renewed
定期支付成功
subscription.past_due
支付失败,订阅存在风险
order.paid
一次性购买完成
refund.created
退款已发起

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 配置

  1. Go to Recur DashboardSettingsWebhooks
  2. Click Add Endpoint
  3. Enter your endpoint URL (e.g.,
    https://yourapp.com/api/webhooks/recur
    )
  4. Select events to receive
  5. Copy the Webhook Secret to your environment variables
  1. 前往 Recur 控制台设置Webhooks
  2. 点击 添加端点
  3. 输入你的端点URL(例如:
    https://yourapp.com/api/webhooks/recur
  4. 选择要接收的事件
  5. Webhook 密钥复制到你的环境变量中

Testing Webhooks Locally

本地测试Webhooks

Using ngrok

使用ngrok

bash
undefined
bash
undefined

Start ngrok tunnel

启动ngrok隧道

ngrok http 3000
ngrok http 3000

Use the ngrok URL in Recur dashboard

在Recur控制台中使用ngrok URL

undefined
undefined

Using Recur CLI (if available)

使用Recur CLI(如果可用)

bash
undefined
bash
undefined

Forward webhooks to local server

将webhooks转发到本地服务器

recur webhooks forward --to localhost:3000/api/webhooks/recur
undefined
recur webhooks forward --to localhost:3000/api/webhooks/recur
undefined

Best 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
id
to deduplicate:
typescript
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可能会多次投递。使用事件
id
来避免重复处理:
typescript
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

相关技能

  • /recur-quickstart
    - Initial SDK setup
  • /recur-checkout
    - Implement payment flows
  • /recur-entitlements
    - Check subscription access after webhook
  • /recur-quickstart
    - 初始SDK设置
  • /recur-checkout
    - 实现支付流程
  • /recur-entitlements
    - Webhook后检查订阅权限