How to Receive HubSpot Webhooks in Next.js
App Router Route Handler with raw body access
Next.js is the most-deployed full-stack JavaScript framework in 2026. The App Router's Route Handlers are a natural fit for webhook endpoints, but the default body parsing strips the raw bytes you need for HMAC verification — you have to read the request stream yourself. This guide walks through the Next.js setup for HubSpot webhooks end to end: capturing the raw body, verifying the signature, handling retries idempotently, and iterating locally without redeploying. Cross-reference the HubSpot Webhooks overview for the event catalog and sample payload.
HubSpot Official Webhook Docs1. Set Up the Next.js Endpoint
The endpoint needs to do three things, in this order: read the raw body, verify the signature against those exact bytes, and only then parse the JSON for your business logic.
// app/api/webhooks/[service]/route.ts
import { NextRequest } from "next/server";
export const runtime = "nodejs"; // signature verification needs Node, not Edge
export const dynamic = "force-dynamic"; // always run, never cache
export async function POST(req: NextRequest) {
// IMPORTANT: read the raw body BEFORE you parse JSON.
// HMAC verification has to run against the bytes the sender signed.
const rawBody = await req.text();
const signature = req.headers.get("x-signature-header") ?? "";
// 1. Verify the signature against the raw body
// 2. Parse JSON only after verification passes
// 3. Process the event idempotently (use the event id as your key)
const event = JSON.parse(rawBody);
console.log("Verified webhook:", event.type ?? event);
return new Response("ok", { status: 200 });
}2. Verify the HubSpot Signature
- Algorithm
- HMAC-SHA256
- Header
X-HubSpot-Signature-v3- Encoding
- base64
HubSpot v3 signs the concatenation of `{HTTP_METHOD}{request_URI}{raw_request_body}{X-HubSpot-Request-Timestamp}` with your app's Client Secret. URL-encoded characters in the URI must be decoded before signing (except the `?` that begins the query string). Reject requests older than 5 minutes.
Node.js verification
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post(
'/webhooks/hubspot',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-hubspot-signature-v3'] as string | undefined;
const timestamp = req.headers['x-hubspot-request-timestamp'] as string | undefined;
if (!signature || !timestamp) return res.status(401).send('missing headers');
// Replay protection — reject requests older than 5 minutes.
const fiveMinutes = 1000 * 60 * 5;
if (Math.abs(Date.now() - Number(timestamp)) > fiveMinutes) {
return res.status(401).send('stale request');
}
// Reconstruct the URI HubSpot signed (decode percent-encoded chars).
const uri = decodeURIComponent(req.originalUrl);
const stringToSign =
req.method + 'https://' + req.headers.host + uri + req.body.toString('utf8') + timestamp;
const expected = crypto
.createHmac('sha256', process.env.HUBSPOT_CLIENT_SECRET!)
.update(stringToSign)
.digest('base64');
if (
signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return res.status(403).send('invalid signature');
}
res.json({ ok: true });
},
);Wire this verification call into the Next.js handler from section 1. The pattern is identical across Next.js versions: read raw body, verify, parse JSON, dispatch.
See HubSpot's official signing docs for the canonical reference, or the cross-service signature verification guide for the same pattern in Ruby and other languages.
3. Make the Handler Idempotent
HubSpot can — and will — send the same event twice. Network blips, your server returning a 5xx mid-processing, deploy windows: any of these triggers a retry, and your handler will see the same event id again. Build for that on day one rather than chasing duplicate-charge bugs in production.
The simplest pattern is a unique constraint on the event id in your database. The handler does the work inside a transaction, and the insert into the events table is the last step — if a retry arrives, the unique-constraint violation tells you the event already committed and you can return 200 without re-running the side effects.
Pattern in any framework:
- Read raw body, verify signature.
- Begin transaction.
- Apply business logic (charge, fulfil, notify, etc.).
- Insert event id into
processed_eventswith a unique constraint. - Commit. Return 200.
- On unique-constraint violation, return 200 — the event was already processed by a prior delivery.
4. HubSpot Retry Behaviour
- Max attempts
- 10
- Total window
- Up to ~8 hours
- Backoff
- Exponential, starts after a few minutes
- Retries on
- 5xx, 429, timeouts (5s)
- Stops on
- Any 2xx response within 5s
Combine the retry numbers above with the idempotency pattern in section 3: aim to acknowledge fast (return 200 under the timeout) and let the idempotency table absorb any duplicates from in-flight retries. The full pattern, including dead-letter queues and replay-from-capture, lives in the Webhook Retry Strategies guide.
5. Test Locally Without Deploying
The fastest iteration loop for any webhook handler is: capture a real HubSpot event with HookRay, then replay that captured request against your local Next.js server until the verification + business logic both pass. No need to retrigger the event in HubSpot, no need to redeploy.
- Get a free webhook URL at hookray.com — no signup.
- Paste the URL into your HubSpotdashboard's webhook settings.
- Trigger a test event. HookRay shows the headers, raw body, and parsed payload in real time.
- Use HookRay's replay feature to send the captured request against
http://localhost:3000/api/webhooks/hubspot(or wherever your Next.js app is listening) — iterate on your code without re-poking the HubSpot dashboard.
Deploying the Next.js Handler
Vercel runs Route Handlers on either the Node.js or Edge runtime. Pin runtime = "nodejs" for any handler that does HMAC verification — Edge's Web Crypto subset has surprising gaps for older signing schemes.
Need a host that boots quickly enough to absorb webhook bursts? DigitalOcean droplets stay warm, support raw-body proxies cleanly, and avoid the cold-start traps of some serverless runtimes.
Capture a real HubSpot webhook in 30 seconds
Free webhook URL, real-time payload inspection, one-click replay. No signup required.
Start Testing — Free