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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "recur-webhooks" with this command: npx skills add recur-tw/skills/recur-tw-skills-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)

EventWhen Fired
checkout.completedPayment successful, subscription/order created
subscription.activatedSubscription is now active
subscription.cancelledSubscription was cancelled
subscription.renewedRecurring payment successful
subscription.past_duePayment failed, subscription at risk
order.paidOne-time purchase completed
refund.createdRefund 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

  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

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

recur-entitlements

No summary provided by upstream source.

Repository SourceNeeds Review
General

recur-quickstart

No summary provided by upstream source.

Repository SourceNeeds Review
General

recur-checkout

No summary provided by upstream source.

Repository SourceNeeds Review
General

recur-help

No summary provided by upstream source.

Repository SourceNeeds Review