Loading...
Loading...
Set up and handle Recur webhook events for payment notifications. Use when implementing webhook handlers, verifying signatures, handling subscription events, or when user mentions "webhook", "付款通知", "訂閱事件", "payment notification".
npx skill4agent add recur-tw/skills recur-webhooks| 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 |
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'// 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
}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 })
}
)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
}
}https://yourapp.com/api/webhooks/recur# Start ngrok tunnel
ngrok http 3000
# Use the ngrok URL in Recur dashboard
# https://xxxx.ngrok.io/api/webhooks/recur# Forward webhooks to local server
recur webhooks forward --to localhost:3000/api/webhooks/recuridasync 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() }
})
}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 })
}console.log('Webhook received:', {
type: event.type,
id: event.id,
timestamp: event.timestamp,
})/recur-quickstart/recur-checkout/recur-entitlements