Agent skill

Lokalise Webhooks Skill

Receive and authenticate Lokalise webhooks. Use when setting up a Lokalise webhook handler, checking the X-Secret header, switching to X-Api-Key or a custom header, debugging 401s or the setup ping, or handling events like project.translation.updated, project.keys.added and project.task.closed.

Install this skill

npx skills add hookdeck/webhook-skills --skill lokalise-webhooks


Lokalise (translation management) sends webhook events as HTTP POST requests when translations, keys, tasks and files change in a project. Lokalise does not sign the payload: it sends the webhook secret, unchanged, in a request header (X-Secret by default), so the endpoint authenticates each request by comparing that header with its stored secret.

When to Use This Skill

  • How do I receive Lokalise webhooks?
  • How do I verify the Lokalise X-Secret header?
  • Does Lokalise sign webhook payloads? (No: shared secret in a header)
  • How do I use X-Api-Key or a custom header instead of X-Secret?
  • Why does the Lokalise webhook ping fail when I save the webhook?
  • How do I handle project.translation.updated or project.keys.added events?

Verification (core)

Lokalise settingHeader received
Secret (default)X-Secret: <secret>
Secret, sent as API keyX-Api-Key: <secret>
Custom header<your header name>: <your value>
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyLokaliseRequest(headers, config = process.env) {
  const secret = config.LOKALISE_WEBHOOK_SECRET;
  const headerName = (config.LOKALISE_WEBHOOK_HEADER || 'X-Secret').toLowerCase();
  if (!secret) return false;                 // never accept when unconfigured
  const received = headers[headerName];      // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);
}

Return 401 when this fails and 200 once handled. There is no signature, so the body is not part of verification, but read it raw (express.raw({ type: '*/*' })) and authenticate before parsing so any content type works. Python uses hmac.compare_digest the same way; see references/verification.md.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The JSON body carries the event name in event (also sent in the X-Event header).

EventFires When
project.translation.updatedA translation was modified
project.translations.updatedTranslations changed in a bulk action (up to 300 per event)
project.key.added / project.keys.addedA key was added / keys were added in bulk
project.key.modified / project.keys.modifiedA key was modified / keys were modified in bulk
project.keys.deletedKeys were deleted
project.task.closedA task was closed (manually or on completion)
project.importedTranslation files were uploaded to the project

The full list (exports, branches, languages, contributors, orders, tasks) is in references/overview.md. Lokalise also sends a ping when a webhook is created; it only needs a 2xx. Respond within 8 seconds or Lokalise retries for up to 24 hours, then disables the webhook.

Environment Variables

LOKALISE_WEBHOOK_SECRET=your-lokalise-webhook-secret   # Apps > Webhooks > your webhook
LOKALISE_WEBHOOK_HEADER=X-Secret                       # or X-Api-Key / your custom header name

Local Development

Lokalise can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 lokalise --path /webhooks/lokalise

Use the printed URL as the webhook URL in the Lokalise Webhooks app.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →