Webhooks
Receive real-time HTTP notifications when content, translations, or assets change in Localess — and trigger downstream systems automatically.
Localess can send an HTTP POST request to any URL you configure whenever a significant event occurs — a content publish, a translation update, an asset upload. Use webhooks to keep downstream systems in sync without polling the API.
Common uses:
- Trigger a build or deploy when content is published (Vercel, Netlify, GitHub Actions)
- Invalidate ISR or CDN caches when a content document goes live
- Send a Slack notification when a translation batch is published
- Kick off search re-indexing after new content is published
Setting Up a Webhook
- Navigate to Developers → Webhooks in the Localess admin UI.
- Click Add Webhook.
- Fill in the form:
| Field | Required | Description |
|---|---|---|
| Name | ✅ | A label for this webhook (e.g. Vercel Production Deploy) |
| URL | ✅ | The HTTPS endpoint that will receive the POST request — see URL requirements |
| Secret | — | A shared secret used to sign the payload. Strongly recommended — see Verifying Signatures |
| Events | ✅ | One or more event types that trigger this webhook |
- Click Save. The webhook is created enabled and starts receiving events immediately.
To pause a webhook without deleting it, choose Disable from its actions menu. A disabled webhook receives nothing until you enable it again.
URL requirements
Localess validates the URL before every delivery and refuses to connect when it:
- is not
https:// - uses a port other than the default
443 - contains credentials (
https://user:pass@…) - points at an internal host —
localhost,*.localhost,*.internal, or a private, loopback, link-local or cloud-metadata address
The address check also applies to the IP the hostname resolves to, so a public name pointing at a private address is refused too. A refused URL is recorded in the delivery log as a network failure.
Events
Localess fires webhooks on the following events:
| Event | Value | Triggered when |
|---|---|---|
CONTENT_PUBLISHED | content.published | A content document is published |
CONTENT_UNPUBLISHED | content.unpublished | A content document is unpublished |
CONTENT_CHANGED | content.changed | A content document is saved (its draft is rebuilt) or deleted |
TRANSLATION_PUBLISHED | translation.published | Translations are published |
TRANSLATION_CHANGED | translation.changed | The translations draft is regenerated after a translation is added, updated or deleted |
Select only the events your endpoint needs. Unneeded events are never delivered to that endpoint, which keeps your handler logic simple.
Payload Format
Every webhook request is an HTTP POST with a JSON body and the content type application/json.
Request headers
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Localess-WebHook/1.0 |
X-Webhook-Event | The event name, e.g. content.published |
X-Webhook-Delivery | A unique UUID for this delivery |
X-Webhook-Signature | HMAC-SHA256 signature of the raw body using your secret, as sha256=<hex-digest>. Present only when a secret is configured |
Example payload — content.published
{
"event": "content.published",
"spaceId": "abc123",
"timestamp": "2026-09-29T12:00:00.000Z",
"data": {
"id": "doc_7xKp9mNq",
"fullSlug": "blog/how-to-get-started"
}
}Every content.* event carries the same data: the document's id and fullSlug.
Example payload — translation.published
{
"event": "translation.published",
"spaceId": "abc123",
"timestamp": "2026-09-29T12:04:00.000Z",
"data": {}
}translation.* events carry an empty data object — they signal that the space's translations changed, not which ones.
The payload identifies what changed; it does not include the document's name, locale, schema or content. Fetch the content from the API if your handler needs more.
Verifying Signatures
When you set a secret on a webhook, Localess signs every request body with HMAC-SHA256 using that secret. Verify the signature before processing the payload to confirm the request came from Localess and was not tampered with in transit.
The signature is in the X-Webhook-Signature header in the format sha256=<hex-digest>. It covers the exact raw body, so verify it before parsing the JSON.
Verification example — Node.js
import crypto from 'crypto';
function verifyLocalessWebhook(
rawBody: string | Buffer,
signature: string,
secret: string
): boolean {
const expected = `sha256=${crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// In a Next.js route handler:
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get('X-Webhook-Signature') ?? '';
if (!verifyLocalessWebhook(rawBody, signature, process.env.LOCALESS_WEBHOOK_SECRET!)) {
return new Response('Unauthorized', { status: 401 });
}
const payload = JSON.parse(rawBody);
// handle payload...
return new Response('OK', { status: 200 });
}Always use timingSafeEqual — standard string comparison is vulnerable to timing attacks.
Delivery and Timeouts
Each event is delivered to each matching webhook once — failed deliveries are not retried.
- Your endpoint has 30 seconds to respond.
- Any status outside
2xxcounts as a failure, including3xx: redirects are not followed, so point the webhook at the final URL. - Every attempt, successful or not, is recorded in the delivery log.
If your endpoint was unavailable, trigger the action again from Localess — for example by republishing the document. Use the X-Webhook-Delivery header to recognise a delivery you have already processed.
Integration Examples
Trigger a Vercel deploy
Vercel provides a Deploy Hook URL in your project settings. Point a Localess content.published webhook at it directly — no code required.
https://api.vercel.com/v1/integrations/deploy/prj_xxxx/yyyyyyyySend a Slack notification
// Slack incoming webhook handler
export async function POST(request: Request) {
const payload = await request.json();
if (payload.event === 'content.published') {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `📝 Content published: ${payload.data.fullSlug}`,
}),
});
}
return new Response('OK', { status: 200 });
}Trigger Next.js ISR revalidation on publish
// app/api/localess-webhook/route.ts
import { revalidatePath } from 'next/cache';
export async function POST(request: Request) {
const rawBody = await request.text();
if (!verifyLocalessWebhook(rawBody, request.headers.get('X-Webhook-Signature') ?? '', process.env.LOCALESS_WEBHOOK_SECRET!)) {
return new Response('Unauthorized', { status: 401 });
}
const payload = JSON.parse(rawBody);
if (payload.event === 'content.published') {
// Revalidate the specific page that changed
revalidatePath(`/${payload.data.fullSlug}`);
}
if (payload.event === 'translation.published') {
// Revalidate all pages that use translations
revalidatePath('/', 'layout');
}
return new Response('OK', { status: 200 });
}Trigger search re-indexing
export async function POST(request: Request) {
const payload = await request.json();
if (payload.event === 'content.published') {
// Fetch the published document and push it to your search index
const content = await localessClient.getContentById(payload.data.id, { locale: 'en' });
await searchClient.upsert({
id: payload.data.id,
title: content.data.title,
body: content.data.body,
url: `/${payload.data.fullSlug}`,
});
}
if (payload.event === 'content.unpublished') {
// Otherwise the index keeps serving pages that are no longer published
await searchClient.delete(payload.data.id);
}
return new Response('OK', { status: 200 });
}The payload doesn't say which locales changed — re-index every locale you serve.
Delivery Logs
Open Developers → Webhooks → [webhook name] to inspect the history of every event sent to that endpoint. The log lists each delivery's ID, event type, status, duration and time. Expand a row to see:
- the failure type —
http(your endpoint replied with an error status),timeout(no response within 30 seconds) ornetwork(the request could not be sent, or the URL was refused) - the HTTP status code and error message
- the request size and the
datathat was sent - the first 4 KB of your endpoint's response body
Use this view to debug failed deliveries or verify that your endpoint is receiving events correctly.