Agent skill

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

Stars 0
Forks 0

Install this agent skill to your Project

npx add-skill https://github.com/recur-tw/skills/tree/main/skills/recur-webhooks

Metadata

Additional technical details for this skill

author
recur
version
0.0.7

SKILL.md

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

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

Expand your agent's capabilities with these related and highly-rated skills.

recur-tw/skills

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

0 0
Explore
recur-tw/skills

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

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

0 0
Explore
recur-tw/skills

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

0 0
Explore
recur-tw/skills

recur-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 串接", "金流設定".

0 0
Explore
mattpocock/skills

edit-article

Edit and improve articles by restructuring sections, improving clarity, and tightening prose. Use when user wants to edit, revise, or improve an article draft.

111,310 9,758
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results