Verifying Contentful Webhook Signatures

This guide, part of the Contentful Integration Guide, secures the endpoint that receives Contentful webhooks. Without verification, anyone who finds the URL can trigger revalidations, rebuilds or purges, and some handlers can be tricked into worse. Contentful supports signed requests: each webhook call carries an HMAC signature over a canonical form of the request, together with the signed headers and a timestamp. The guide shows how to enable signing, verify requests correctly, reject replays, rotate secrets and handle several environments.

Webhook endpoints are easy to overlook in security reviews because they only “trigger a rebuild”. But a rebuild endpoint that anyone can call is a cheap denial-of-service against build minutes and API quotas, and handlers that read entry data from the payload and act on it, such as writing to a search index, can be fed forged content. Verification makes the payload trustworthy, so the handler can act on it without re-fetching everything from the API.

A signed Contentful webhookContentful builds a canonical request from method, path, signed headers and body, signs it with the shared secret and sends it with signature, timestamp and signed-headers headers; the handler rebuilds the canonical request, compares signatures in constant time, checks the timestamp, and only then processes the event.ContentfulWebhook handlerRevalidationcanonical request +HMAC-SHA256(secret)POST + x-contentful-signature,|timestamp, signed-headersrebuild canonical request,compare, check agerevalidate entry tags200
Both sides compute the same canonical request; only a holder of the secret can produce a matching signature.

The Problem

A retailer’s revalidation endpoint accepted any POST with a JSON body containing an entry id and revalidated the matching pages. A crawler that found the URL in a public repository’s example configuration started posting to it every few seconds with random ids, which kept the site’s data cache churning and drove API usage up by an order of magnitude until someone noticed the rate limit errors. Separately, a handler that updated the site search index from webhook payloads could have been fed arbitrary content by anyone who guessed the format.

How Contentful Request Signing Works

A signing secret per webhook. In the webhook settings, Contentful generates or accepts a signing secret. Store it in your secret manager; Contentful shows it only once.

Canonical request. For each call, Contentful builds a canonical string from the HTTP method, the request path including query, a selected set of headers and the body, and computes an HMAC-SHA256 over it with the secret.

Headers on the request. The call carries x-contentful-signature with the signature, x-contentful-signed-headers listing which headers were included, and x-contentful-timestamp with the signing time, which is itself a signed header.

Verification. The receiver rebuilds the canonical request from what it received, computes the HMAC with its copy of the secret, compares in constant time, and rejects requests whose timestamp is older than a small window. Contentful’s @contentful/node-apps-toolkit provides a verifyRequest function that does exactly this.

What each check protects againstSignature verification, timestamp window, raw body use, per-environment secrets and idempotency compared on the attack or failure each one prevents.CheckPreventsSignature over canonical requestforged or modified requestsTimestamp windowreplay of captured requestsRaw body, unparsedfalse failures from re-serialized JSONSecret per environmentstaging events accepted as productionIdempotency by eventduplicate processing on retries
Each check closes a different gap; together they make webhook payloads trustworthy.

Implementation

The handler reads the raw body, verifies with the toolkit and only then parses the payload. Verification must use the exact bytes received; parsing and re-serializing JSON changes whitespace and key order and breaks the signature.

TypeScript
// app/api/contentful-webhook/route.ts
import { verifyRequest } from "@contentful/node-apps-toolkit";
import { revalidateTag } from "next/cache";

const MAX_AGE_SECONDS = 60;

export async function POST(req: Request) {
  const rawBody = await req.text();
  const url = new URL(req.url);
  const headers = Object.fromEntries([...req.headers.entries()].map(([k, v]) => [k.toLowerCase(), v]));

  let valid = false;
  try {
    valid = verifyRequest(
      process.env.CONTENTFUL_WEBHOOK_SECRET!,
      { method: "POST", path: url.pathname + url.search, headers, body: rawBody },
      MAX_AGE_SECONDS,
    );
  } catch {
    valid = false; // missing headers, expired timestamp or malformed signature
  }
  if (!valid) return new Response("invalid signature", { status: 401 });

  const event = headers["x-contentful-topic"] ?? "";          // e.g. ContentManagement.Entry.publish
  const payload = JSON.parse(rawBody) as { sys: { id: string; type: string; contentType?: { sys: { id: string } }; environment?: { sys: { id: string } } } };

  if (/Entry\.(publish|unpublish|delete|archive)$/.test(event)) {
    revalidateTag(`entry:${payload.sys.id}`);
    if (payload.sys.contentType) revalidateTag(`type:${payload.sys.contentType.sys.id}`);
  } else if (/Asset\.(publish|unpublish|delete)$/.test(event)) {
    revalidateTag(`asset:${payload.sys.id}`);
  }
  return Response.json({ ok: true }, { status: 202 });
}

Behind a proxy or platform that rewrites paths, the path the handler sees may differ from the path Contentful signed. Verify with the public path configured in Contentful’s webhook URL, or configure the proxy to preserve it; a mismatch here is the most common cause of every request failing verification after deployment.

Several environments

Webhooks from staging and production environments should use different secrets, so a leaked staging secret cannot trigger production work. Either configure separate webhooks per environment with their own secrets and endpoint paths, or route both to one handler that tries each environment’s secret and uses the one that verifies to decide the target, as described in routing webhooks by environment. Never decide the environment from an unsigned header or payload field before verifying.

Rotating secrets

Rotate signing secrets on a schedule and whenever someone with access leaves. To avoid dropped events, accept two secrets during the rotation: add the new secret to the handler’s list, update the webhook in Contentful to sign with it, confirm deliveries succeed, then remove the old secret. The handler simply tries each configured secret in turn.

Idempotency after verification

Verification proves a request came from Contentful, but not that it is new: Contentful retries deliveries that time out or fail, and the same event can arrive more than once. Make the actions after verification idempotent. Revalidating a tag twice is harmless, which is one reason tag-based invalidation suits webhooks well. Actions that are not naturally idempotent, such as incrementing a counter or sending a notification, need deduplication by an event identifier or by entry id and revision, stored for a day, as described in idempotent webhook handlers. Respond quickly after verification and enqueue slow work, so deliveries never time out and trigger retries in the first place.

Configuration Reference

Setting Recommendation Why
Signing enabled on every webhook Unverified endpoints can be abused.
Secret storage secret manager, per environment Isolation and rotation.
Verification input raw body, public path, lowercased headers Matches what Contentful signed.
Timestamp window 30 to 60 seconds Blocks replays, tolerates clock skew.
Topics publish, unpublish, delete, archive Saves and auto-saves are noise for the public site.
Response 202 after verification, fast Avoids retries from timeouts.

Gotchas & Edge Cases

  • Body parsing middleware. Frameworks that parse JSON before your handler runs lose the raw body. Read the raw text in the handler, or disable parsing for this route.
  • Header casing. Header names must be lowercased consistently for the canonical form; the toolkit expects that.
  • Clock skew. Servers with drifting clocks reject valid requests as expired. Keep time synchronized and do not make the window too tight.
  • Test deliveries. Contentful’s “test” webhook call is signed too; use it to confirm verification works after each deployment.

Worked Example

The retailer enabled signing on its production and staging webhooks with separate secrets, replaced its endpoint with a verified handler, and moved the search-index update to act only on verified payloads. The unsolicited requests continued for a few days and were all rejected with 401 at negligible cost; API usage returned to normal the same day. A later secret rotation, with both secrets accepted for an hour, completed without a single failed delivery.

Revalidations per hour at the webhook endpointRevalidations performed per hour before signing, when unsolicited requests triggered them freely, and after verification, when only genuine Contentful events did.Before verification2400 revalidations per hourAfter verification35 revalidations per hour
Verification removed the forged traffic without affecting real publishes.

Debugging Failed Verification

When every request fails verification after a deployment, the cause is almost always a mismatch between what Contentful signed and what the handler reconstructs. Log, for a failing request, the path the handler used, the list of signed headers and whether each is present, the body length and the timestamp age, never the secret or signature itself. Compare the path with the webhook URL configured in Contentful: trailing slashes, query parameters added by platforms and path rewrites by proxies are frequent culprits. Check that no middleware parsed and re-serialized the body. Send a test delivery from Contentful after each fix. Once verification works, keep a metric of rejected requests; a sudden rise usually means a configuration change broke verification, not an attack.

Rollout Checklist

  • Enable request signing on every Contentful webhook, with a secret per environment.
  • Verify the raw body with the official helper and a short timestamp window.
  • Parse and act on the payload only after verification.
  • Subscribe to publish, unpublish, delete and archive topics for public caches.
  • Support two secrets during rotation.
  • Log and alert on verification failures.

Frequently Asked Questions

Is a secret header enough instead of signing?

A static secret header stops casual abuse but can be replayed indefinitely once leaked, and it does not protect the body from modification. Signing is stronger and just as easy to set up.

Do we still need to re-fetch the entry after a webhook?

For cache invalidation, no; revalidation fetches fresh data anyway. For actions based on payload content, verified payloads can be trusted.

What status should failed verification return?

401, without details about why verification failed. Contentful will retry, which is harmless for invalid requests and helpful if the failure was a temporary configuration problem.

Does verification slow the handler down?

Computing one HMAC takes microseconds; the cost is negligible compared with the network round trip, and far smaller than the cost of processing forged requests.