Refresh your application when editors publish#
A webhook is an HTTP POST from Contoprix to your application after a selected CMS event. The most common use is refreshing a cached page when an editor publishes a change.
Editor publishes /about
-> Contoprix queues a signed webhook
-> your app verifies the signature
-> your app invalidates the relevant cache
-> visitors receive the new published pageWebhooks are asynchronous. Your handler must be safe to receive the same delivery more than once.
Create an endpoint safely#
When a website webhook endpoint is created, Contoprix generates a signing secret and returns it in the creation response. Copy it immediately into your application's secret store. The normal endpoint response does not expose the secret afterward.
Choose:
- a clear name, such as
Production Next.js revalidation; - a publicly reachable destination URL for your application;
- only the events the destination needs; and
- an active or inactive state.
The destination must be an absolute HTTP or HTTPS URL that resolves to a public global address. Localhost, private-network addresses, metadata hosts, and URL-embedded credentials are blocked. Use HTTPS for production. For local development, expose a temporary public URL rather than configuring localhost as the destination.
Events that are currently dispatched#
| Event | When it is sent | Useful data in data |
|---|---|---|
page.published | A page version is published | pageId, slug, languageCode, publishedAtUtc |
content.published | A content entry version is published | entryId, contentTypeId, slug, versionId, publishedAtUtc |
media.uploaded | A media item is uploaded | mediaId, fileName, url, mimeType, size |
content.review.submitted | An entry enters review | Workflow and entry information |
content.review.approved | A review is approved | Workflow and entry information |
content.review.changes_requested | A reviewer asks for changes | Workflow and entry information |
content.review.cancelled | A review is cancelled | Workflow and entry information |
Event data can grow over time. Read the fields your handler needs and ignore unfamiliar fields so additions do not break it.
Request format#
Contoprix sends a JSON body similar to this:
{
"id": "delivery-event-id",
"event": "page.published",
"websiteId": "website-id",
"timestamp": "2026-08-22T10:15:30.0000000+00:00",
"data": {
"pageId": "page-id",
"slug": "/about",
"languageCode": "en",
"publishedAtUtc": "2026-08-22T10:15:29.0000000+00:00"
}
}It also sends these headers:
| Header | Meaning |
|---|---|
x-contoprix-signature | Lowercase hexadecimal HMAC-SHA256 signature of the exact raw UTF-8 body |
x-contoprix-event | Internal event enum name, such as PagePublished |
x-contoprix-delivery-id | The delivery record ID, useful for idempotency logs |
Next.js: the quickest safe handler#
Install the Next.js integration, then add a route handler:
import { handleContoprixWebhook } from "@contoprix/next/server";
export async function POST(request: Request) {
return handleContoprixWebhook(request, {
secret: process.env.CONTOPRIX_WEBHOOK_SECRET!,
onPagePublished(event) {
console.info("Published page", event.data.slug);
},
onContentPublished(event) {
console.info("Published content", event.data.entryId);
},
});
}The helper verifies x-contoprix-signature against the raw body using a timing-safe comparison, parses the envelope, and returns appropriate 401 or 400 responses for bad requests. By default, it also revalidates the published page path and Contoprix page, navigation, and content tags for page.published and content.published events.
Set the same generated signing secret in your deployment environment:
CONTOPRIX_WEBHOOK_SECRET=the-secret-returned-when-the-endpoint-was-createdVerify manually in another server framework#
The important rule is to verify the raw request body before parsing JSON. Parsing and re-serializing changes the bytes being signed.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isValidContoprixWebhook(
rawBody: string,
signature: string | null,
secret: string,
) {
if (!signature) return false;
const supplied = signature.trim().replace(/^sha256=/i, "").toLowerCase();
if (!/^[a-f0-9]{64}$/.test(supplied)) return false;
const expected = createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const suppliedBuffer = Buffer.from(supplied, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
return suppliedBuffer.length === expectedBuffer.length &&
timingSafeEqual(suppliedBuffer, expectedBuffer);
}After verification, parse the JSON, record the event or delivery ID, and perform the smallest needed cache refresh or background action.
Make processing idempotent#
Contoprix marks a 2xx HTTP response as delivered. Failed deliveries can be retried, up to 10 total attempts, with increasing delays (about 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours, then daily retries).
Store a processed event ID or x-contoprix-delivery-id before doing irreversible work:
If delivery ID was already processed:
return 200
Store delivery ID as processing
Refresh the relevant cache or queue the work
Mark delivery ID completed
Return 200Do not use a webhook to trigger arbitrary URLs, clear every cache entry, or trust unverified request data.
Recommended responses#
| Situation | Response | Why |
|---|---|---|
| Signature is missing or invalid | 401 | Tells Contoprix the request was not accepted. |
| JSON body is invalid | 400 | The payload cannot be processed. |
| Delivery already completed | 200 | Makes retries safe. |
| Cache refresh or queue succeeded | 200 or another 2xx | Marks the delivery complete. |
| Temporary database or provider outage | 5xx | Allows a retry. |
Keep the handler fast. Queue slow work such as search indexing, image analysis, or third-party synchronization after signature verification.
Troubleshooting#
| Problem | Check this |
|---|---|
| No delivery reaches the app | Confirm the endpoint is active, public, and configured for the event. |
| Destination is rejected when configured | It may resolve to localhost, a private address, an unsafe host, or contain embedded credentials. |
| Every request returns 401 | Confirm the raw body is verified with the exact generated signing secret. |
| The page remains stale | Confirm the handler revalidates the route or cache tag matching data.slug. |
| Duplicate work happens | Deduplicate by envelope id or x-contoprix-delivery-id. |
| Delivery keeps retrying | Return a 2xx only after the event is safely accepted or queued. Check endpoint delivery logs for status and error details. |
Use SDK server helpers for day-to-day data access, and use a webhook only to react to a real CMS event after it has been verified.