JSTAcademy
0 XP
Dashboard
Crash Courses
Webhooks & Event-Driven APIs
10 min
PhD+200 XP
Crash Courses · PhD

Webhooks & Event-Driven APIs

Implement webhooks to push real-time events and verify signature security.
10 min read+200 XP on completionCert: REST APIs Crash Course
Tap any word in the text below to start reading from there.

Webhooks & Event-Driven APIs

Webhooks let third-party services notify you of events in real time. You build the receiver; they call it.

Webhook Receiver with Signature Verification

// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'

function verifySignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

export async function POST(req: NextRequest) {
  // Get raw body BEFORE parsing JSON
  const rawBody = await req.text()
  const signature = req.headers.get('x-stripe-signature') ?? ''

  if (!verifySignature(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(rawBody)
  const idempotencyKey = event.id

  // Check for duplicate event
  const already = await db.processedEvents.findUnique({ where: { id: idempotencyKey } })
  if (already) return NextResponse.json({ received: true })

  // Process event
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePayment(event.data.object)
      break
  }

  // Mark as processed
  await db.processedEvents.create({ data: { id: idempotencyKey } })
  return NextResponse.json({ received: true })
}

// Disable body parsing (Next.js default) — we need raw text
export const config = { api: { bodyParser: false } }

Sending Webhooks (Outbound)

async function sendWebhook(url: string, event: object, secret: string) {
  const payload = JSON.stringify(event)
  const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex')

  await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
    },
    body: payload,
  })
}
0%