This tutorial will show you how to use the Lokalise Node SDK to create webhooks, listen to webhook events in third-party apps, and handle the incoming notifications.


In this tutorial you'll learn how to...

  • Register webhooks with the Lokalise API
  • Listen to incoming notifications
  • React to webhooks notifications

You can find the source code on GitHub.


This guide assumes that you have a Lokalise project (if not, learn how to create your Lokalise project here). Also, you'll need a read/write Lokalise API token; you can learn how to get one in the corresponding article.

If you want to follow this guide locally on your computer, you need to have the following software installed:

Finally, please note that in order to listen and respond to webhook events, your app must be publicly accessible. In this tutorial, we'll deploy our app to Heroku (see below).

What we are going to build

We are going to create a simple application that will allow users to register webhooks in their Lokalise projects. The app will also listen to incoming notifications generated by these webhooks and react to them by sending API requests.

Preparing a new app


About Node SDK v9

Please note that starting from version 9 our Node SDK is a pure ESM module. It does not provide a CommonJS export (require) anymore. Therefore you should either convert your project to ESM, use dynamic import, or use version 8 which we are fully supporting.

To get started, create a new Node app:

mkdir YOUR_PROJECT && cd YOUR_PROJECT && npm init -y

Now install the necessary dependencies:

npm install --save connect-flash cookie-session express hbs @lokalise/node-api

Add a start script to package.json and set the app type to module:

"type": "module",
"scripts": {
  "start": "node index.js"

Also add a Procfile with the following content:

web: node index.js

This file will be used to boot your app on Heroku. If you are using a different hosting provider, you might need to take additional steps.

Project skeleton

Now let's prepare the necessary files. First of all, create index.js in the project root:

import cookieSession from 'cookie-session'
import express from 'express'
import flash from 'connect-flash'
import { router } from "./config/routes.js"
import { setupAssets } from "./config/assets.js"

const app = express()
app.use(express.urlencoded({extended: true}))
  name: 'session',
  secret: 'my_super_secret',
  maxAge: 24 * 60 * 60 * 1000

app.use('/', router)

const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log(`Express web app on port ${port}`)


This is the main file where we load all the necessary modules, set up session store, routes, etc.

Now create a config/assets.js file:

import hbs from "hbs"
import path from "path"

const __dirname = path.resolve()

export function setupAssets(app) {
  app.set('view engine', 'hbs')
  hbs.registerPartials(path.join(__dirname, 'views/partials'))

We'll be using the Handlebars templating engine, but of course you can take advantage of any other solution.

The next step is adding the root route inside the config/routes.js file:

import express from "express"

import { StaticPagesController } from "../controllers/staticPagesController.js"

export const router = express.Router()

router.get('/', (req, res) => {
  StaticPagesController.index(req, res)

Create controllers/staticPagesController.js file:

import { ApplicationController } from "./application_controller.js"

export class StaticPagesController extends ApplicationController { 
  static index(req, res) {
    this.renderView(req, res, 'static_pages/index', {title: 'Register Lokalise webhook'})

This controller inherits from ApplicationController, so let's also add it now under controllers/applicationController.js:

export class ApplicationController {
  static renderView(req, res, view, data = {}) {
    data.flash_messages = req.flash('data')
    res.render(view, data)

Next, let's take care of the main layout under views/layout.hbs:

<!DOCTYPE html>
    <meta charset="utf-8">

      {{> header}}


Also add views/partials/header.hbs:


{{#each flash_messages as |msg|}}
  {{#each msg}}

Finally, add a views/static_pages/index.hbs file:

<form method="POST" action="/webhooks">
  <label for="project_id"></label>
  <input type="text" name="project_id" id="project_id">
  <input type="submit" value="Register">

We are simply displaying a form to register a new webhook in the specified Lokalise project.

Before wrapping up this section, let's initialize a new Git repository and create a new Heroku project:

git init

That's it, now let's proceed to the next section!

Registering webhooks

The next step is to actually register a webhook for the specified project ID. Therefore, create a new route:

// ...
import { WebhooksController } from "../controllers/webhooksController.js"

// ...'/webhooks', (req, res) => {
  WebhooksController.create(req, res)

Now add a webhooksController.js file to the controllers directory:

import { ApplicationController } from "./application_controller.js"

export class WebhooksController extends ApplicationController { 
  static async create(req, res) {
    try {
      await this.lokaliseApi().webhooks().create(
        {url: process.env.NOTIFY_URL, events: ['project.key.added']},
        {project_id: req.body.project_id}
    } catch(e) {

    req.flash('data', {success: `Registered webhook for ${req.body.project_id} project ID`})

We use the lokaliseApi() method which should be defined inside the ApplicationController, as below:

import { LokaliseApi } from '@lokalise/node-api'

export class ApplicationController {
  // ...
  static lokaliseApi() {
    return new LokaliseApi({ apiKey: process.env.LOKALISE_API_TOKEN})

Additionally, note that we are using two environment variables so let's add those to Heroku:

heroku config:add LOKALISE_API_TOKEN=123abc NOTIFY_URL=https://YOUR_HEROKU_PROJECT/notify


Your app must be publicly available!

You won't be able to register a new Lokalise webhook if your app is running locally. That's because Lokalise will try to send a special "ping" request to make sure the provided notification URL is actually available.

Listening to events

The last thing to do is listen to incoming Lokalise events, therefore create a new route:

// ...

import { NotificationsController } from "../controllers/notificationsController.js"

// ...'/notify', (req, res) => {
  NotificationsController.create(req, res)

Create a new notificationsController.js file:

import { ApplicationController } from "./application_controller.js"

export class NotificationsController extends ApplicationController {
  static async create(req, res) {
    const payload = req.body
    if (Array.isArray(payload)) {
      if (payload[0] === "ping") {
      } else {

    if(payload.event === 'project.key.added') {
      await this.lokaliseApi().comments().create([
          { comment: "@Bob please review this new key" }
        { project_id:, key_id: }

      await this.lokaliseApi().keys().update(, {
        "is_hidden": true
      }, { project_id: })


First of all, we are making sure that the received request is not "ping" (otherwise do nothing). If the request event is project.key.added then we are adding a new comment for this key and hiding it from non-admins.

You can introduce an additional level of security by checking the request headers. Specifically, each reqest with contain a special "secret" (that you can customize when creating a webhook) as well as the Lokalise project ID and the webhook ID. Please find more information in the corresponding document.


A notice on notification handlers

Please note that your notification handler routes must respond to HTTP POST and return 2xx status codes. Otherwise, Lokalise will try to re-send notifications multiple times before giving up.

Testing it

Now everything is ready! Deploy your app on Heroku:

git commit -m "Initial"
git push heroku master
heroku open

Enter your Lokalise project ID in the text input and press "Register".

Now open Lokalise, proceed to your project, and click "Apps" in the top menu. Find the "Webhooks" app, click on it, and then press "Manage". You'll see that a new webhook was registered for you:

Note the "X-Secret header" hidden value. You can use this value inside your app and compare it with the one sent to the notify route for extra protection (thus filtering out malicious requests).

Now return to the Lokalise project editor and create a new translation key. Reload the page and make sure the key was hidden:

Click on the "Comments" button (the first button on the screen above) and make sure the comment is displayed properly:

That's it, great job!

Webhooks and bulk actions

Webhooks support Lokalise bulk actions as well as the find/replace operation. You can find more info the Webhooks article but let's see how to listen to such events.

First of all, it's important to enable the proper events in the webhooks configuration. For instance, if you'd like to monitor find/replace operation as well as applying pseudolocalization using bulk actions, you should tick the translation updated event:

Please keep in mind that as long as the bulk actions may involve multiple keys, the event payload will contain data from 1 and up to 300 keys. If more than 300 keys were involved in the operation, you'll receive multiple events.

Also note that in case of bulk actions, the event name will be pluralized: project.translations.updated, not project.translation.updated. For example, if you would like to display information about all the updated translations as well as project and user name, you can use the following code snippet:

static async create(req, res) {
  const payload = req.body
  if (Array.isArray(payload)) {
    if (payload[0] === "ping") {
    } else {

  if(payload.event === 'project.translations.updated') {
    console.log(`Project name: #{`)
    console.log(`User name: #{payload.user.full_name`)

    payload.translations.forEach((translation) => {
      console.log(`Translation ID: #{`)
      console.log(`Translation value: #{translation.value`)
      console.log(`Language: #{`)
      console.log(`Key: #{`)


So, payload.translations contain an array with all the translations data, including the language and key details.