Flags guides
04 · Webhooks
Hear about every change.
Post Production flag changes to a channel, refresh caches the moment a config is published, and know when a key rotates.
You need flags.webhook.manage to create endpoints. Signatures, retries, disabling and replay work the same for every ProductCraft product: see the shared Webhooks page.
1
The two events
config.published: a change produced a new revision of a config in an environment. It carries the project, config and environment, the new and previous revision, the ETag, the change (id, action, note, who made it) and the keys of the settings that changed. A change that touches three environments (a new flag) sends three events.sdk_key.rotated: a key was rotated or its grace period ended. It carries the slot, the key prefix and when the old key stops working, never the key.
2
Add an endpoint
Filter an endpoint by project, config or environment so it only hears what it needs. The signing secret is shown once, when the endpoint is created.
create-endpoint.ts
import { Flags } from "@productcraft/flags";
const flags = new Flags(); // PRODUCTCRAFT_API_KEY + PRODUCTCRAFT_WORKSPACE_ID
declare const projectId: string;
declare const productionId: string;
const endpoint = await flags.webhooks.endpoints.create({
url: "https://hooks.acme.com/flags",
description: "Production flag changes to #releases",
event_types: ["config.published", "sdk_key.rotated"],
project_id: projectId,
environment_id: productionId,
});
// endpoint.signing_secret (whsec_…) is in this answer only: store it
// in your secret manager now.
console.log(endpoint.id, endpoint.signing_secret_hint); // never log the secret itself3
Verify and read
Events are signed with Standard Webhooks. flags.webhooks.unwrap() checks the signature against the raw body and returns a typed event; it reads the secret from PRODUCTCRAFT_FLAGS_WEBHOOK_SECRET.
app/webhooks/flags/route.ts
// app/webhooks/flags/route.ts (Next.js route handler)
import { Flags } from "@productcraft/flags";
// Reads PRODUCTCRAFT_FLAGS_WEBHOOK_SECRET
const flags = new Flags();
export async function POST(req: Request) {
const event = await flags.webhooks.unwrap({ payload: await req.text(), headers: req.headers });
switch (event.type) {
case "config.published":
console.log(
`${event.data.environment.name}: revision ${event.data.revision}`,
event.data.settings_changed,
event.data.change.note,
);
break;
case "sdk_key.rotated":
console.log(event.data.reason, event.data.key_prefix, event.data.expires_at);
break;
default:
break; // event types added later
}
return new Response(null, { status: 204 });
}4
Order and duplicates
- Events can arrive out of order. Order
config.publishedbyrevisionper config and environment, and ignore one older than what you have seen. - A retry repeats the same
webhook-idheader: use it to skip duplicates. - The webhook is a notification, not the record: History and the stored revisions have everything, so a missed delivery loses nothing.