recur-webhooks
Recur Webhook Integration
You are helping implement Recur webhooks to receive real-time payment and subscription events.
Webhook Events
Core Events (Most Common)
| Event | When 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 |
All Supported Events
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
Next.js App Router
// 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
}
Express.js
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 })
}
)
Event Payload Structure
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
}
}
Webhook Configuration
- 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
Testing Webhooks Locally
Using ngrok
# Start ngrok tunnel
ngrok http 3000
# Use the ngrok URL in Recur dashboard
# https://xxxx.ngrok.io/api/webhooks/recur
Using Recur CLI (if available)
# Forward webhooks to local server
recur webhooks forward --to localhost:3000/api/webhooks/recur
Best Practices
1. Always Verify Signatures
Never trust webhook payloads without verifying the signature.
2. Handle Idempotency
Webhooks may be delivered multiple times. Use the event id to deduplicate:
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() }
})
}
3. Return 200 Quickly
Process events asynchronously to avoid timeouts:
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 })
}
4. Handle Retries Gracefully
Recur retries failed webhook deliveries. Ensure your handler is idempotent.
5. Log Everything
console.log('Webhook received:', {
type: event.type,
id: event.id,
timestamp: event.timestamp,
})
Debugging Webhooks
Check Webhook Logs
In Recur Dashboard → Webhooks → Click endpoint → View delivery logs
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
Related Skills
/recur-quickstart- Initial SDK setup/recur-checkout- Implement payment flows/recur-entitlements- Check subscription access after webhook
More from recur-tw/skills
recur-entitlements
Implement access control and permission checking with Recur entitlements API. Use when building paywalls, checking subscription status, gating premium features, or when user mentions "paywall", "權限檢查", "entitlements", "access control", "premium features".
32recur-quickstart
Quick setup guide for Recur payment integration. Use when starting a new Recur integration, setting up API keys, installing the SDK, or when user mentions "integrate Recur", "setup Recur", "Recur 串接", "金流設定".
31recur-checkout
Implement Recur checkout flows including embedded, modal, and redirect modes. Use when adding payment buttons, checkout forms, subscription purchase flows, or when user mentions "checkout", "結帳", "付款按鈕", "embedded checkout".
29recur-help
List all available Recur skills and how to use them. Use when user asks "what can Recur do", "Recur skills", "Recur 有什麼功能", "help with Recur", "如何使用 Recur skills".
28recur-portal
Implement Customer Portal for subscription self-service. Use when building account pages, letting customers manage subscriptions, update payment methods, view billing history, or when user mentions "customer portal", "帳戶管理", "訂閱管理", "更新付款方式", "self-service".
27