Localess

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

  1. Navigate to Developers → Webhooks in the Localess admin UI.
  2. Click Add Webhook.
  3. Fill in the form:
FieldRequiredDescription
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
  1. 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:

EventValueTriggered when
CONTENT_PUBLISHEDcontent.publishedA content document is published
CONTENT_UNPUBLISHEDcontent.unpublishedA content document is unpublished
CONTENT_CHANGEDcontent.changedA content document is saved (its draft is rebuilt) or deleted
TRANSLATION_PUBLISHEDtranslation.publishedTranslations are published
TRANSLATION_CHANGEDtranslation.changedThe 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

HeaderDescription
Content-Typeapplication/json
User-AgentLocaless-WebHook/1.0
X-Webhook-EventThe event name, e.g. content.published
X-Webhook-DeliveryA unique UUID for this delivery
X-Webhook-SignatureHMAC-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 2xx counts as a failure, including 3xx: 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/yyyyyyyy

Send 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) or network (the request could not be sent, or the URL was refused)
  • the HTTP status code and error message
  • the request size and the data that 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.

On this page