genko.meDocs
Publishing and integrations

Webhooks

Register a destination, verify signatures, and handle events, test sends, and delivery failures.

A webhook notifies an external URL with an HTTP POST when content changes. Use it to rebuild your site or revalidate its cache. It doesn't deliver entry content. It sends a small JSON message that tells you something changed.

You need the Owner or Maintainer role and an HTTPS receiving URL that's reachable from outside.

1. Prepare a destination

Prepare a deploy hook on your hosting provider, or a receiving handler in your own app. To use it for Next.js revalidation, you can use nextRevalidate from Plugins.

In your own handler, decide what to update when a notification arrives. Make sure it can also handle a deleted entry and test notifications that have no entry.

2. Register the webhook

  1. Open the target API's Settings (/w/your-workspace/apis/blog/settings).
  2. Select "Add webhook" and enter a name and the destination URL.
  3. To verify signatures, set a secret.
  4. Select Add and confirm it's enabled.

You can register up to 5 per API. You can't choose events one by one. Each event of the registered API is sent, so filter out the ones you don't need on the receiving side. To pause one, switch it off.

Register the same secret in your receiving side's settings. It's a different value from the API key. If you leave it out, the signature header isn't sent.

Destinations must be HTTPS. Prepare a receiving URL that's reachable from outside. Redirects aren't followed, so set the final receiving URL. Destinations that resolve to private, loopback, or other non-public addresses are rejected, so you can't point a webhook at localhost or an internal network. The signing secret is stored encrypted.

3. Send a test

From the action menu in the list, choose "Send test", then check your receiver's log and the send result. A 2xx response counts as delivered. In a test, event is webhook.test and entry is null.

A test send doesn't resend an event that failed earlier. To check the behavior on a real entry update, save the target entry and verify all the way to the receiving side.

Events

EventTriggered by
entry.createdA new save, including creating a draft
entry.updatedSaving an existing entry, a manual status change, restoring from history, and so on
entry.deletedDeleting content
entry.publishedA scheduled publish settled by the periodic job
entry.unpublishedA publish end by date settled by the periodic job
webhook.testA test send

When you set an entry to Published by hand and save, it's a create or update event. If you limit your publishing handling to entry.published, you'll miss manual publishes. Also, when an entry goes from published to draft, the new state in entry.updated is draft, so you also need to invalidate the old published page.

Saving a published entry with "Save as draft", or discarding unpublished changes, doesn't send a webhook. Publishing unpublished changes with Save sends a regular entry.updated.

Payload and headers

json
{  "event": "entry.updated",  "workspace": "your-workspace",  "api": "blog",  "entry": { "id": "abcDEF123456", "status": "published" },  "triggeredAt": "2026-09-01T01:00:00.000Z"}

entry.status is draft, published, scheduled, or closed. A deletion notice carries the state before deletion. If you need the content, fetch it from the delivery API when you need it.

A notification for a change to an additional-language version of a multilingual API has a top-level lang with the language code. Notifications for the main language don't. entry.id is the same across languages. For example, saving the English version looks like this.

json
{  "event": "entry.updated",  "workspace": "your-workspace",  "api": "blog",  "lang": "en",  "entry": { "id": "abcDEF123456", "status": "published" },  "triggeredAt": "2026-09-01T01:00:00.000Z"}

On the receiving side, check whether lang is present, and update the per-language URLs and caches. This covers creating, updating, and deleting a language version, as well as a settled scheduled publish and publish end. The payload for the existing main language doesn't change.

HeaderValue
content-typeapplication/json
user-agentgenko-webhook/1.0
x-genko-eventThe event name
x-genko-timestampUnix seconds for this delivery attempt, when a secret is set
x-genko-signaturesha256= plus HMAC-SHA256 hex of timestamp.rawBody, when a secret is set

Verify the signature

This is an example receiving handler that uses the web-standard Request and Response. Verify the signature against the raw body, before you parse it as JSON. The SDK rejects timestamps more than 300 seconds from the current time by default and compares signatures in constant time. A resend gets a fresh timestamp and signature.

ts
import { verifyGenkoSignature } from "@genko-me/sdk";
export async function POST(request: Request) {  const secret = process.env.GENKO_WEBHOOK_SECRET;  if (!secret) return new Response("Webhook secret is not configured", { status: 500 });
  const rawBody = await request.text();  const valid = await verifyGenkoSignature({    secret,    rawBody,    signatureHeader: request.headers.get("x-genko-signature"),    timestampHeader: request.headers.get("x-genko-timestamp"),  });  if (!valid) return new Response("Invalid signature", { status: 401 });
  const payload = JSON.parse(rawBody);  // Validate the workspace, api, event, and the shape of any values you use before handling it.  return Response.json({ received: true, event: payload.event });}

This example only confirms receipt. In practice, add what you need, such as revalidation or adding the work to a queue. Don't wait on heavy work synchronously, and design the handler so that processing the same update twice doesn't break anything.

Delivery limits and failures

The timeout is 10 seconds, and there are no automatic retries. From the action menu in the list, open "Delivery log" and choose a delivery. Under "Request sent" and "Response received" you can see the URL, headers, and body, the HTTP status, the time taken, and any connection error.

Records are kept for 30 days. The response body is kept up to the first 4 KB, and the log shows when a longer body was truncated. Part of each request signature is masked, and the response's Set-Cookie isn't recorded.

ResultWhat to check
301 / 302, and so onDid you register the final URL after the redirect?
401Does the secret match? Are you verifying against the raw body?
404Is the receiving route deployed to production?
TimeoutCan you respond within 10 seconds?
Succeeded but the page is staleAre the rebuild or revalidation targets after receipt correct?

After you fix the receiver, select "Resend" on that delivery log to send it again by hand. The resent body is the same as the original, and triggeredAt keeps its original time. The destination URL and signing secret come from the current webhook settings, and the new delivery result is recorded.

A resend can deliver the same event more than once, so make your receiver able to handle duplicates. For notifications past the retention period, have a way to refetch the latest data or run a rebuild by hand. A successful test send alone doesn't mean the failed processing has recovered.

Last updated: October 16, 2026

On this page