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 Settings → 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 |
| 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. Localess immediately stores the webhook and starts delivering events.
Events
Localess fires webhooks on the following events:
| Event | Trigger |
|---|---|
content.published | A content document is promoted from Draft to Published |
content.deleted | A content document is deleted |
translations.published | The Publish action is taken in the Translations module |
assets.uploaded | One or more asset files are uploaded |
assets.deleted | An asset file is 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 |
X-Localess-Event | The event name, e.g. content.published |
X-Localess-Delivery | A unique UUID for this delivery attempt |
X-Localess-Signature | HMAC-SHA256 signature of the raw body using your secret. Present only when a secret is configured |
X-Localess-Timestamp | Unix timestamp (seconds) of when the event was generated |
Example payload — content.published
{
"event": "content.published",
"deliveryId": "e4a2c891-3f10-4b2d-9c5a-1d8f63b2a471",
"timestamp": 1748131200,
"spaceId": "abc123",
"data": {
"documentId": "doc_7xKp9mNq",
"name": "How to Get Started",
"slug": "how-to-get-started",
"fullSlug": "blog/how-to-get-started",
"locale": "en",
"schema": "article"
}
}Example payload — translations.published
{
"event": "translations.published",
"deliveryId": "b1c3d592-8e47-4f9a-bc12-2e7a94f1d830",
"timestamp": 1748131440,
"spaceId": "abc123",
"data": {
"locales": ["en", "fr", "de"]
}
}Example payload — assets.uploaded
{
"event": "assets.uploaded",
"deliveryId": "f9d2a104-5c63-4e8b-a731-3b9c72e8f502",
"timestamp": 1748131600,
"spaceId": "abc123",
"data": {
"assets": [
{
"assetId": "asset_3rNm7qLx",
"name": "hero-banner.jpg",
"uri": "/spaces/abc123/assets/hero-banner.jpg",
"mimeType": "image/jpeg",
"size": 284931
}
]
}
}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-Localess-Signature header in the format sha256=<hex-digest>.
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-Localess-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.
Retry Logic
If your endpoint does not respond with an HTTP 2xx status within 10 seconds, Localess marks the delivery as failed and retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 8 hours |
After 5 failed attempts the delivery is abandoned. The delivery log in Settings → Webhooks → [webhook name] → Deliveries records every attempt, the HTTP status received, and the response body, so you can investigate failures.
Make your endpoint idempotent — use the X-Localess-Delivery header as an idempotency key so that retried deliveries do not cause duplicate actions.
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.name}* (${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-Localess-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 === 'translations.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 to your search index
const content = await localessClient.getContentBySlug(payload.data.fullSlug, {
locale: payload.data.locale,
});
await searchClient.upsert({
id: payload.data.documentId,
title: content.data.title,
body: content.data.body,
url: `/${payload.data.fullSlug}`,
});
}
return new Response('OK', { status: 200 });
}Delivery Logs
Navigate to Settings → Webhooks → [webhook name] → Deliveries to inspect the history of every event sent to that endpoint. Each delivery record shows:
- The event type and delivery ID
- The HTTP status code returned by your endpoint
- The timestamp and duration
- The full request payload and response body
Use this view to debug failed deliveries or verify that your endpoint is receiving events correctly.