Gareth Wilson Gareth Wilson

Guide to Lokalise Webhooks: Features and Best Practices

Published


Lokalise webhooks tell your systems when something changes in a localisation project: translations are updated or proofread, keys are added or modified, a file is imported or exported, a task is closed, a branch is merged, or a translation order completes. They are how teams trigger builds, pull new translations into a repository, or notify reviewers as soon as work is done in Lokalise.

This guide covers how Lokalise webhooks are set up and authenticated, the events and payloads, Lokalise's 24-hour retry window and what happens when it runs out, and the best practices for running a Lokalise receiver in production.

What are Lokalise webhooks?

Lokalise webhooks are configured per project, through the Webhooks app or the Webhooks API. Each handler has a URL, a set of events and an authentication header. When a subscribed event happens, Lokalise sends a JSON POST to the URL.

Lokalise states that it doesn't sign its requests. Instead, it sends a secret in a header with every request:

  • By default, a random alphanumeric secret is generated when the app is enabled and sent in X-Secret. You can regenerate it or type in your own value.
  • You can send your own value in X-Api-Key instead.
  • Or you can send a custom header with a name and value of your choice.

Your receiver authenticates a request by comparing the header with the value it expects. Every request also carries Project-Id and Webhook-Id headers that identify the project and the handler.

Lokalise webhook features

FeatureDetails
ConfigurationPer project: Apps > Webhooks, or the Webhooks API
Method and formatPOST, JSON
AuthenticationA secret in X-Secret (default), a value in X-Api-Key, or a custom header
SignatureNone. Lokalise doesn't sign requests
Other headersProject-Id, Webhook-Id
Event nameevent in the body
Setup requestA ping when a handler is configured, which needs a 2xx
Response windowA 2xx within 8 seconds
RetriesAfter about 1, 5, 10, 20 and 30 minutes, then hourly, for up to 24 hours
After 24 hoursAccumulated events are deleted and the handler is disabled
OrderingBest effort, not guaranteed
Bulk events1 to 300 objects per event. Larger actions arrive as several events

Lokalise publishes the IP addresses it sends from and suggests restricting requests to them: 3.67.82.138, 3.123.244.162, 3.124.254.35, 18.185.175.152, 18.192.113.205 and 18.194.146.34. The same list covers all outbound traffic from Lokalise.

Common events

Lokalise's event names are dotted and mostly start with project.:

AreaEvents
Translationsproject.translation.updated, project.translations.updated, project.translation.proofread, project.translations.proofread
Keysproject.key.added, project.keys.added, project.key.modified, project.keys.modified, project.keys.deleted, project.key.comment.added
Filesproject.imported, project.exported
Projectproject.copied, project.snapshot
Languagesproject.languages.added, project.language.removed, project.language.settings_changed
Contributorsproject.contributor.added, project.contributor.added_public, project.contributor.deleted
Tasksproject.task.created, .queued, .closed, .deleted, .language.closed, .initial_tm_leverage.calculated
Branchesproject.branch.added, project.branch.deleted, project.branch.merged
Ordersteam.order.created, team.order.completed, team.order.deleted

Singular and plural names are different events. project.translation.updated fires for one translation. project.translations.updated fires for a bulk action, such as applying translation memory, and carries up to 300 translations. The same pattern applies to keys. Bulk events include an action field, such as bulk.tm or bulk.add_tags, that says what caused them.

Here is Lokalise's project.translation.updated example:

{
  "event": "project.translation.updated",
  "translation": {
    "id": 84835169,
    "value": "Hello, %s!",
    "previous_value": "Hi!",
    "segment": 1
  },
  "language": { "id": 640, "iso": "en", "name": "English" },
  "key": {
    "id": 16307701,
    "name": "test.title",
    "filenames": { "ios": null, "android": null, "web": "%LANG_ISO%.yml", "other": null }
  },
  "project": {
    "id": "138c1ffa0ad94848f01f980e7f2f2af19d1bd553",
    "name": "TheApp Project",
    "branch": "master"
  },
  "user": { "email": "jdoe@mycompany.com", "full_name": "John Doe" },
  "created_at": "2019-07-29 12:18:31",
  "created_at_timestamp": 1564395511
}

Most payloads share event, project (with the branch), user, created_at and created_at_timestamp. created_at has no time zone, so use created_at_timestamp, which is Unix seconds.

The setup guide also lists "Project deleted" among the events you can subscribe to, but the events reference documents no payload or name for it.

Setting up Lokalise webhooks

  1. In your Lokalise project, open Apps, find Webhooks, and click Install.
  2. Enter the URL that events should be sent to.
  3. Keep the default X-Secret header option. Copy the generated secret, or type your own, and store it server-side, for example as LOKALISE_WEBHOOK_SECRET.
  4. Select the events you want.
  5. Click Enable app, or Save changes if you're adding a handler to an existing app.

Lokalise sends a ping to the URL when the handler is configured, and your endpoint must answer it with a 2xx. Lokalise doesn't document the ping's body or whether it carries the secret header. If enabling the app fails against a receiver that checks the secret, log the ping's headers and body to see what arrived, and answer a ping that carries the right secret with 200 even though it is not an event you handle.

If you use Hookdeck, create a Lokalise source, paste the secret into it, and use the Hookdeck source URL as the webhook URL. Hookdeck checks the X-Secret header, so keep the default X-Secret option. A handler switched to X-Api-Key or a custom header sends no X-Secret and fails verification. Hookdeck accepts every request with a 200 first, which also answers the ping, and checks the header afterwards. A request with a missing or wrong secret is recorded as failed verification and is not delivered to your service.

You can also create handlers through the Webhooks API, which is useful for configuring the same webhook across many projects.

Securing Lokalise webhooks

There is nothing to compute. Compare the X-Secret header with your stored secret in constant time. Because the body isn't signed, you can parse it however you like.

const crypto = require('crypto');
const express = require('express');

function verifyLokaliseSecret(headerValue, expectedSecret) {
  if (!headerValue || !expectedSecret) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(expectedSecret, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/lokalise', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.LOKALISE_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyLokaliseSecret(req.get('x-secret'), secret)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  // Route on event.event and acknowledge anything you don't handle, including the ping.
  // Persist the event, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hmac


def verify_lokalise_secret(header_value, expected_secret: str) -> bool:
    if not header_value or not expected_secret:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), expected_secret.encode("utf-8"))


# In FastAPI: verify_lokalise_secret(request.headers.get("x-secret"),
#                                    os.environ["LOKALISE_WEBHOOK_SECRET"])

The secret travels with every request, so anyone who captures one request can send forged ones. Serve the endpoint over HTTPS only, and consider allowlisting Lokalise's published IP addresses as a second check.

Make Lokalise webhooks production-ready. Hookdeck Event Gateway checks the X-Secret header, accepts every translation and key event as soon as it arrives, and durably queues it, so a long outage on your side doesn't run out Lokalise's 24-hour window and disable the handler.

Lokalise webhook limitations and pain points

A shared secret instead of a signature

The Problem: Nothing proves that the body you received is the body Lokalise sent.

Why It Happens: Lokalise doesn't sign requests. It sends the secret itself in a header, so a captured request can be replayed or altered.

Workarounds:

  • Use HTTPS, a long random secret, and Lokalise's published IP allowlist.
  • Regenerate the secret with the Refresh icon if it might have leaked, and update your receiver at the same time.
  • Before acting on content, such as committing translations, fetch the current data from the Lokalise API.

How Hookdeck Can Help: Hookdeck checks the secret for you, so it lives on the Hookdeck source rather than in every service, and requests with the wrong secret are recorded as failed verification instead of being delivered.

After 24 hours, events are deleted and the handler is disabled

The Problem: A day-long outage loses every event from that day, and nothing arrives afterwards until someone notices.

Why It Happens: Lokalise accumulates events for up to 24 hours while a handler fails. After that it deletes them and disables the handler, which must be re-enabled by hand in the project settings.

Workarounds:

  • Alert on failures early, well before the 24 hours run out.
  • After an outage, re-enable the handler and reconcile through the Lokalise API, for example by downloading the latest files.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, so Lokalise sees successes even while your service is down. Hookdeck then retries delivery to your service on a schedule you control and lets you replay anything that failed.

An 8 second response window

The Problem: Handlers that download files or run builds before responding time out and get retried.

Why It Happens: Lokalise counts a request as failed if it doesn't get a 2xx within 8 seconds.

Workarounds:

  • Respond 200 straight after checking the secret and storing the event.
  • Run builds, exports and pull requests from a queue.

How Hookdeck Can Help: Hookdeck answers Lokalise as soon as the request is stored, without waiting for your service, and your service can take as long as it needs.

Bulk actions arrive in pieces

The Problem: A bulk change of 1,000 keys shows up as several events, and there is no field telling you it's one action or that the last piece has arrived.

Why It Happens: Bulk and replace actions put 1 to 300 objects in each event and split larger actions across multiple events.

Workarounds:

  • Process each event's objects independently, rather than waiting for a complete set.
  • Debounce downstream work, such as a translation export or build, so it runs once after a burst of events instead of once per event.

How Hookdeck Can Help: Hookdeck's rate limiting on your destination smooths out bursts of bulk events so your service isn't flooded.

No delivery id

The Problem: You can't tell a retry from a new event by an id.

Why It Happens: Webhook-Id identifies the handler, not the delivery, and the payloads have no event id.

Workarounds:

  • Build a key from event, project.id, the object IDs (such as translation.id or key.id) and created_at_timestamp.
  • Make handlers idempotent, for example by writing the latest translation value rather than appending changes.

How Hookdeck Can Help: Hookdeck's deduplication can drop identical repeated deliveries based on the fields you choose.

Best practices

Compare the secret in constant time

Use crypto.timingSafeEqual or hmac.compare_digest rather than === or ==, and fail closed if the secret isn't configured.

Branch-aware routing

Payloads include project.branch. If you use Lokalise branching, check the branch before acting, so work on a feature branch doesn't trigger a production build.

Handle singular and plural events

Subscribe to both forms if you need every change: project.translation.updated and project.translations.updated, project.key.added and project.keys.added. Otherwise bulk actions, such as applying translation memory, pass you by.

Acknowledge fast and process asynchronously

See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Lokalise authenticates webhooks with a shared secret in the X-Secret header rather than a signature. Compare it in constant time over HTTPS, answer the setup ping with a 2xx, subscribe to both singular and bulk events, respond within 8 seconds, and watch for failures, because after 24 hours Lokalise deletes the queued events and disables the handler.

Hookdeck Event Gateway sits in front of your receiver, checks the Lokalise secret, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Lokalise webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.