Webhook Configuration & Events

Webhooks let you build real-time integrations with BlackOps Center. Get notified when content is published, users subscribe, payments process, or RSS items are scored—and trigger custom workflows in response.

What Are Webhooks?

A webhook is a server-side HTTP callback. When something happens in BlackOps Center (an "event"), we send a POST request to your specified URL with event data. Your server receives it and does whatever you want—update a CRM, trigger a Slack notification, sync to another system, etc.

Webhooks vs. Polling

❌ Polling (Inefficient)

Every 5 minutes:
"Any new posts?"
"No"
"Any new posts?"
"No"
"Any new posts?"
"Yes! Here's data"

✅ Webhooks (Efficient)

[silence]
[silence]
[POST] "New post published"
[silence]

Available Webhooks

1. Stripe Payment Webhooks

Endpoint: /api/webhooks/stripe

Events:

customer.subscription.created

Fired when a new subscription starts (including trials).

{
  "type": "customer.subscription.created",
  "data": {
    "object": {
      "id": "sub_xxx",
      "customer": "cus_xxx",
      "status": "active",
      "items": {
        "data": [
          {
            "price": {
              "id": "price_base",
              "unit_amount": 29700
            },
            "quantity": 1
          }
        ]
      },
      "trial_end": 1738368000,
      "current_period_start": 1735776000,
      "current_period_end": 1738368000
    }
  }
}

What We Do:

  • Create subscription record in database
  • Calculate site count (base + additional sites)
  • Initialize monthly credits for user
  • Send welcome notification (if configured)

customer.subscription.updated

Fired when subscription changes (upgrade, add sites, billing renewal).

What We Do:

  • Update subscription status and site count
  • Reset monthly credits on billing period renewal
  • Handle tier upgrades/downgrades
  • Deactivate trial if converted to paid

customer.subscription.deleted

Fired when subscription is canceled.

What We Do:

  • Mark subscription as canceled
  • Send cancellation notification to platform admin
  • Retain data (sites go read-only, not deleted)

invoice.payment_succeeded

Fired when payment processes successfully.

What We Do:

  • Log successful payment
  • Update subscription status if needed
  • Optional: Send receipt email

invoice.payment_failed

Fired when payment fails.

What We Do:

  • Send payment failure notification to user
  • Alert platform admin (for follow-up)
  • Update subscription status

checkout.session.completed

Fired when one-time purchase completes (credit packs).

What We Do:

  • Add purchased credits to user account
  • Record transaction in credit history
  • Send purchase confirmation

2. Resend Email Webhooks

Endpoint: /api/webhooks/resend/inbound

email.received

Fired when email is sent to a reservoir's unique address (e.g., react-tips@blackopscenter.com).

{
  "type": "email.received",
  "data": {
    "email_id": "email_xxx",
    "from": "Ben Newton <ben@example.com>",
    "to": ["react-tips@blackopscenter.com"],
    "subject": "Great article on useMemo",
    "text": "Check this out: https://...",
    "html": "<p>Check this out...</p>",
    "received_at": "2025-02-01T10:30:00Z"
  }
}

What We Do:

  • Validate reservoir email address
  • Verify webhook signature (Svix)
  • Fetch full email content from Resend API
  • Extract text/HTML and metadata
  • Create reservoir item from email content
  • Track in reservoir_inbound_emails table
  • Prevent duplicate imports via deleted_content tracking

Endpoint: /api/webhooks/resend/delivery

email.delivered

Fired when newsletter email is successfully delivered.

email.bounced

Fired when email bounces (invalid address, full inbox, etc.).

What We Do:

  • Update subscriber status (bounced)
  • Track bounce reasons
  • Auto-unsubscribe after hard bounce

email.opened

Fired when recipient opens newsletter email.

What We Do:

  • Track open event
  • Update subscriber engagement score
  • Record timestamp for analytics

email.clicked

Fired when recipient clicks link in newsletter.

What We Do:

  • Track click event
  • Record which link was clicked
  • Update engagement metrics

Setting Up Webhooks

Stripe Webhooks (Required for Billing)

Local Development with Stripe CLI

  1. Install Stripe CLI:
    # macOS
    brew install stripe/stripe-cli/stripe
    
    # Or download from stripe.com/docs/stripe-cli
  2. Login to Stripe:
    stripe login
  3. Forward webhooks to local dev:
    stripe listen --forward-to localhost:3000/api/webhooks/stripe
  4. Copy webhook signing secret from CLI output:
    Ready! Your webhook signing secret is whsec_xxx
  5. Add to .env.local:
    STRIPE_WEBHOOK_SECRET=whsec_xxx
  6. Restart dev server
  7. Test:
    stripe trigger customer.subscription.created

Production Setup

  1. Go to Stripe Dashboard → Webhooks
  2. Click "Add endpoint"
  3. Enter endpoint URL:
    https://blackopscenter.com/api/webhooks/stripe
  4. Select events:
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_succeeded
    • invoice.payment_failed
    • checkout.session.completed
  5. Click "Add endpoint"
  6. Reveal signing secret and copy it
  7. Add to production environment variables:
    STRIPE_WEBHOOK_SECRET=whsec_production_xxx

Resend Inbound Email Webhooks

Setup Steps

  1. Go to Resend Dashboard → Webhooks
  2. Click "Create Webhook"
  3. Enter endpoint URL:
    https://blackopscenter.com/api/webhooks/resend/inbound
  4. Select event: email.received
  5. Save webhook
  6. Copy signing secret (Resend uses Svix)
  7. Add to environment:
    RESEND_WEBHOOK_SECRET=whsec_xxx

Configure Inbound Email Domain

  1. In Resend, go to Domains → Inbound
  2. Add MX records to DNS:
    Priority: 10
    Host: @
    Value: inbound.resend.com
  3. Verify domain
  4. Create catch-all forwarding rule in Resend (optional)

Webhook Security

Signature Verification

Always verify webhook signatures to prevent spoofing:

// Stripe signature verification
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

export async function POST(request: Request) {
  const body = await request.text()
  const signature = request.headers.get('stripe-signature')
  
  try {
    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    )
    
    // Process event...
  } catch (err) {
    return new Response('Invalid signature', { status: 400 })
  }
}

Rate Limiting

Resend inbound webhook includes rate limiting:

// Rate limiting (in-memory for serverless)
const rateLimitMap = new Map()
const MAX_REQUESTS = 10 // per minute per reservoir

function checkRateLimit(identifier: string): boolean {
  const now = Date.now()
  const record = rateLimitMap.get(identifier)
  
  if (!record || now > record.resetTime) {
    rateLimitMap.set(identifier, {
      count: 1,
      resetTime: now + 60000
    })
    return true
  }
  
  if (record.count >= MAX_REQUESTS) {
    return false // Rate limit exceeded
  }
  
  record.count++
  return true
}

Payload Size Limits

Protect against large payloads:

const MAX_SIZE = 2 * 1024 * 1024 // 2MB

const contentLength = request.headers.get('content-length')
if (contentLength && parseInt(contentLength) > MAX_SIZE) {
  return new Response('Payload too large', { status: 413 })
}

Webhook Best Practices

1. Respond Quickly

Webhook providers expect 2xx response within 10-30 seconds:

// ✅ Good: Acknowledge immediately, process async
export async function POST(request: Request) {
  const event = await parseWebhook(request)
  
  // Queue for background processing
  await queueJob('process-webhook', { event })
  
  return new Response('Accepted', { status: 202 })
}

// ❌ Bad: Process inline (timeout risk)
export async function POST(request: Request) {
  const event = await parseWebhook(request)
  
  await processEvent(event) // Takes 45 seconds
  
  return new Response('OK', { status: 200 })
}

2. Handle Duplicates

Webhooks may be sent multiple times. Use idempotency:

// Check for duplicate by event ID
const { data: existing } = await supabase
  .from('processed_webhooks')
  .select('id')
  .eq('event_id', event.id)
  .single()

if (existing) {
  return new Response('Already processed', { status: 200 })
}

// Process event...

// Mark as processed
await supabase
  .from('processed_webhooks')
  .insert({ event_id: event.id, processed_at: new Date() })

3. Log Everything

Comprehensive logging helps debugging:

console.log('[Webhook]', {
  type: event.type,
  id: event.id,
  timestamp: new Date().toISOString(),
  source: 'stripe',
  customer: event.data.object.customer
})

// On error
console.error('[Webhook Error]', {
  type: event.type,
  error: error.message,
  stack: error.stack
})

4. Graceful Degradation

Don't fail the entire app if webhook processing fails:

try {
  await processWebhook(event)
} catch (error) {
  // Log error
  console.error('Webhook processing failed:', error)
  
  // Store for manual review
  await supabase
    .from('failed_webhooks')
    .insert({
      event_id: event.id,
      event_type: event.type,
      error_message: error.message,
      payload: event
    })
  
  // Still return 200 to prevent retries
  return new Response('Logged for review', { status: 200 })
}

Testing Webhooks

Local Testing

Use tools like ngrok or Stripe CLI to test locally:

# Option 1: Stripe CLI (recommended)
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Option 2: ngrok
ngrok http 3000
# Use ngrok URL in webhook configuration

Test Events

Trigger test events without real transactions:

stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
stripe trigger checkout.session.completed

Manual Testing with cURL

curl -X POST http://localhost:3000/api/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "stripe-signature: t=xxx,v1=xxx" \
  -d '{
    "type": "customer.subscription.created",
    "data": {
      "object": {
        "id": "sub_test123",
        "customer": "cus_test123",
        "status": "active"
      }
    }
  }'

Monitoring Webhooks

Stripe Dashboard

  1. Go to Stripe → Webhooks
  2. Click on your endpoint
  3. View:
    • Recent Events: Success/failure status
    • Response Times: How fast you respond
    • Retry Attempts: Failed deliveries

Application Logs

Monitor webhook processing in your application logs:

// Example log output
[Webhook] stripe/customer.subscription.created | sub_xxx | 152ms | success
[Webhook] stripe/invoice.payment_succeeded | in_xxx | 87ms | success
[Webhook] resend/email.received | email_xxx | 1234ms | success
[Webhook Error] stripe/checkout.session.completed | Timeout | retry_queued

Alerting

Set up alerts for webhook failures:

  • High Failure Rate: >5% of webhooks fail
  • Slow Response: Response time >10 seconds
  • Signature Errors: Invalid signatures detected

Troubleshooting

Webhook Not Receiving Events

Causes:

  • Wrong endpoint URL
  • Firewall blocking requests
  • Endpoint not publicly accessible

Fixes:

  • Verify URL is correct and publicly accessible
  • Check server logs for incoming requests
  • Test with curl from external server
  • Check firewall/security group rules

"Invalid Signature" Errors

Causes:

  • Wrong webhook secret in environment
  • Body modified before verification
  • Using wrong secret (test vs. prod)

Fixes:

  • Verify STRIPE_WEBHOOK_SECRET matches dashboard
  • Don't parse body as JSON before verification (use raw body)
  • Check you're using the correct secret for environment

Webhooks Timing Out

Causes:

  • Processing takes too long
  • External API calls block response
  • Database queries slow

Fixes:

  • Return 202 immediately, process async
  • Use background job queue
  • Optimize database queries
  • Set reasonable timeout on external calls

Custom Webhooks (Coming Soon)

In future releases, you'll be able to configure custom webhooks for BlackOps Center events:

  • post.published: New blog post goes live
  • post.updated: Existing post edited
  • subscriber.added: New newsletter signup
  • reservoir.item_added: New content saved to reservoir
  • campaign.completed: Content campaign finishes

Configuration will be available in Settings → Webhooks.

Next Steps

Webhooks turn BlackOps Center from a standalone tool into the central hub of your content ecosystem.

Webhook Configuration & Events - BlackOps Center