Webhooks & CORS
GET/POST handler.Receiving webhooks
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
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 })
}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.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
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
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 |
|---|---|
| Which origin(s) may read the response — a specific origin, or |
| Which HTTP methods the caller is allowed to use |
| Which request headers the caller is allowed to send (e.g. |
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.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
OPTIONShandler that returns the sameAccess-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.