Guide to Lokalise Webhooks: Features and Best Practices
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-Keyinstead. - 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
| Feature | Details |
|---|---|
| Configuration | Per project: Apps > Webhooks, or the Webhooks API |
| Method and format | POST, JSON |
| Authentication | A secret in X-Secret (default), a value in X-Api-Key, or a custom header |
| Signature | None. Lokalise doesn't sign requests |
| Other headers | Project-Id, Webhook-Id |
| Event name | event in the body |
| Setup request | A ping when a handler is configured, which needs a 2xx |
| Response window | A 2xx within 8 seconds |
| Retries | After about 1, 5, 10, 20 and 30 minutes, then hourly, for up to 24 hours |
| After 24 hours | Accumulated events are deleted and the handler is disabled |
| Ordering | Best effort, not guaranteed |
| Bulk events | 1 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.:
| Area | Events |
|---|---|
| Translations | project.translation.updated, project.translations.updated, project.translation.proofread, project.translations.proofread |
| Keys | project.key.added, project.keys.added, project.key.modified, project.keys.modified, project.keys.deleted, project.key.comment.added |
| Files | project.imported, project.exported |
| Project | project.copied, project.snapshot |
| Languages | project.languages.added, project.language.removed, project.language.settings_ |
| Contributors | project.contributor.added, project.contributor.added_, project.contributor.deleted |
| Tasks | project.task.created, .queued, .closed, .deleted, .language.closed, .initial_ |
| Branches | project.branch.added, project.branch.deleted, project.branch.merged |
| Orders | team.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
- In your Lokalise project, open Apps, find Webhooks, and click Install.
- Enter the URL that events should be sent to.
- 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. - Select the events you want.
- 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-Secretheader, 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
200straight 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 astranslation.idorkey.id) andcreated_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.