# Recur Webhooks

> 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".

- Skill: `recur-tw/recur-webhooks` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add recur-tw/recur-webhooks`
- Raw SKILL.md: https://api.skillmd.com/api/skills/recur-tw/recur-webhooks/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: recur-tw (https://skillmd.com/u/recur-tw)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/recur-tw/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

```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

### Next.js App Router

```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
}
```

### 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 })
  }
)
```

## 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
  }
}
```

## Webhook Configuration

1. Go to **Recur Dashboard** → **Settings** → **Webhooks**
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

## Testing Webhooks Locally

### Using ngrok

```bash
# 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)

```bash
# 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:

```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() }
  })
}
```

### 3. Return 200 Quickly

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 })
}
```

### 4. Handle Retries Gracefully

Recur retries failed webhook deliveries. Ensure your handler is idempotent.

### 5. Log Everything

```typescript
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

