Test Contentful webhooks locally
Receive Contentful webhook events on localhost with a Horizon tunnel, and verify the signed request with verifyRequest.
Receive Contentful events on your laptop while you build, with a URL Contentful can reach.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - A Contentful space where you can manage webhooks
Start your app
Contentful can sign webhook requests. It adds the x-contentful-signature, x-contentful-signed-headers and x-contentful-timestamp headers. The signature is an HMAC SHA-256 (a keyed hash) of the method, path, signed headers and body, using your signing secret.
Contentful's verifyRequest helper in @contentful/node-apps-toolkit checks all of it. Install it.
npm install @contentful/node-apps-toolkitverifyRequest returns true or false. It throws when the request is shaped wrong or older than the time-to-live, which is 30 seconds by default.
import { verifyRequest } from "@contentful/node-apps-toolkit";
export async function POST(request: Request) {
const secret = process.env.CONTENTFUL_SIGNING_SECRET;
if (!secret) {
return new Response("Missing CONTENTFUL_SIGNING_SECRET", { status: 500 });
}
const body = await request.text();
const { pathname, search } = new URL(request.url);
let isValid = false;
try {
isValid = verifyRequest(secret, {
method: request.method,
path: `${pathname}${search}`,
headers: Object.fromEntries(request.headers),
body,
});
} catch (error) {
console.error("Could not verify request:", error);
}
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
const topic = request.headers.get("x-contentful-topic");
console.log(`Received Contentful event: ${topic}`);
return new Response("ok", { status: 200 });
}You get the secret in the dashboard step below. Create the file now.
CONTENTFUL_SIGNING_SECRET=replace-with-your-signing-secretStart the app on port 3000.
npm run devStart a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your Contentful webhook would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appYour public URL is https://my-app.hrzn.run. Keep this terminal open.
Turn on request verification
- In your space, open Settings, then Webhooks.
- Select the Settings tab.
- Select Enable request verification.
- Copy the signing secret into
CONTENTFUL_SIGNING_SECRETand restartnpm run dev.
Add the webhook in Contentful
- In Settings, Webhooks, select Add Webhook.
- Enter a name.
- Enter
https://my-app.hrzn.run/api/webhooks/contentfulas the URL, and select thePOSTmethod. - Pick the events that trigger the webhook, for example publishing an entry.
- Set the webhook to active and save it.
Trigger an event
Contentful's docs describe no resend button for webhook calls. Trigger a real event instead: publish an entry in the space.
To inspect a call, open the webhook's overview and select View details on an event. It shows the JSON and your server's response. Contentful keeps up to 500 log entries per webhook.
Check it works
Publish an entry. Horizon prints one line for the request:
POST 200 /api/webhooks/contentfulYour app terminal prints the topic from the X-Contentful-Topic header, in the form ContentManagement.Entry.publish:
Received Contentful event: ContentManagement.Entry.publishIf the line shows [401], see Troubleshooting.
Troubleshooting
The signature doesn't match
- Check that
CONTENTFUL_SIGNING_SECRETis the secret from Enable request verification, and restartnpm run devafter you edit.env.local. - Pass the raw body to
verifyRequest. Don't passJSON.stringifyof a parsed body, because that can change the bytes. - Read the error in your app terminal.
verifyRequestthrows when the request is older than its 30-second time-to-live. Check your computer's clock.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.
Nothing reaches your app
- Check that the Horizon terminal is still running. If its last line is
Connection lost. Reconnecting…, wait forReconnected. - Check that the webhook is active and that its URL ends with
/api/webhooks/contentful.
Next steps
- Read Contentful's guide to request verification.
Test Calendly webhooks locally
Receive Calendly webhook events on localhost with a Horizon tunnel, create the subscription through the API, and verify Calendly-Webhook-Signature.
Test DocuSign Connect webhooks locally
Receive DocuSign Connect events on localhost with a Horizon tunnel, and verify the HMAC signature in a Next.js route handler.