NextjsWebhooks & CORS

Webhooks & CORS

Route Handlers aren't only for your own frontend to call. Two common outside callers are third-party services pushing webhooks at your app, and browsers on a different origin making cross-origin requests to your API. Both need a bit of extra care beyond a plain GET/POST handler.
Receiving webhooks
A webhook is just an HTTP POST request that a third party (Stripe, GitHub, a payment gateway, a CMS) sends to a URL you registered with them, whenever some event happens on their side. A webhook receiver is a normal Route Handler — the only twist is that you don't control the caller, so you have to trust the payload carefully.

app/api/webhooks/payments/route.ts

TSX
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
import { verifySignature } from '@/libs/webhooks'

export async function POST(request: Request) {
  const rawBody = await request.text()
  const signature = headers().get('x-webhook-signature')

  const isValid = verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET!)

  if (!isValid) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(rawBody)
  await handlePaymentEvent(event)

  return NextResponse.json({ received: true })
}
Always verify the signature
Never trust a webhook payload just because it arrived on the right URL — anyone on the internet can POST to a public endpoint. Providers sign their payloads with a shared secret (often via an x-*-signature header); verify that signature against the raw request body before parsing or acting on the event. Read the body with request.text(), not request.json(), so you have the exact bytes the signature was computed over.
It's also good practice to respond quickly — return a 200 as soon as you've validated and queued the event, and do slow work (sending emails, updating multiple tables) asynchronously. Most providers retry aggressively if they don't get a fast 2xx response.
CORS: letting other origins call your API
By default, browsers block a page on example-a.com from reading a response from api.example-b.com unless that response explicitly opts in with CORS (Cross-Origin Resource Sharing) headers. If you're building a public API that other websites or mobile apps will call directly from the browser, you need to add those headers yourself.

app/api/public/products/route.ts

TSX
import { NextResponse } from 'next/server'

const CORS_HEADERS = {
  'Access-Control-Allow-Origin': 'https://partner-site.com',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

export async function GET() {
  const products = await getProducts()
  return NextResponse.json(products, { headers: CORS_HEADERS })
}

// Browsers send an OPTIONS "preflight" request before certain cross-origin
// requests (e.g. ones with custom headers or non-GET/POST methods).
export async function OPTIONS() {
  return new NextResponse(null, { status: 204, headers: CORS_HEADERS })
}

Header

Purpose

Access-Control-Allow-Origin

Which origin(s) may read the response — a specific origin, or * for any (only for fully public, non-authenticated data)

Access-Control-Allow-Methods

Which HTTP methods the caller is allowed to use

Access-Control-Allow-Headers

Which request headers the caller is allowed to send (e.g. Authorization)

Handle the preflight OPTIONS request
For requests with a custom header, a JSON content type, or a method like PUT/DELETE, browsers first send an automatic OPTIONS preflight request to ask permission. If your Route Handler doesn't export an OPTIONS function that responds with the right CORS headers, the browser blocks the real request before it's ever sent.
Note
If your API is only ever called by your own Next.js app's pages and components — the common case — you don't need any CORS configuration at all. CORS only matters when the calling page is served from a different origin than the API. Same-origin fetch calls from your own frontend to your own Route Handlers work without any of this.
  • Webhook receivers are normal Route Handlers, but the caller is untrusted — always verify a cryptographic signature against the raw request body before acting on the payload.

  • Respond quickly (a fast 2xx) to webhooks and defer slow work, since most providers retry on timeout.

  • CORS headers are only needed when a browser page on a different origin calls your API directly.

  • Export an OPTIONS handler that returns the same Access-Control-Allow-* headers to satisfy preflight requests.

  • Same-origin calls from your own app's frontend to your own Route Handlers never need CORS configuration.