Zendesk provides help center JSON Web Tokens (JWTs) to help you verify client-side requests to third-party APIs from a help center. You can use these JWTs to create help center integrations that can securely access user data from a third-party service.

In this tutorial, you'll create a to-do list integration using a custom page of your help center. The integration uses API requests to let users load, add, and remove to-do items stored on a third-party web server.

The integration doesn't share data across users. Users can only access their own to-do items.

As part of the tutorial, you'll also set up the third-party web server. The server verifies the signatures of help center JWTs included in incoming API requests to ensure they're legitimate. If so, the server then uses the Zendesk user id from the JWT's payload to access and return to-do items for the user.

Disclaimer: Zendesk provides this article for instructional purposes only. Zendesk doesn't provide support for the integration or example code in this tutorial. Zendesk doesn't support third-party technologies, such as Node.js or related libraries.

What you'll need

To complete this tutorial, you'll need the following:

Though not required, Zendesk recommends using a sandbox for testing. As part of the tutorial, you'll edit the help center theme's code to add a custom page. We recommend making these changes in a sandbox rather than a production instance.

Creating the custom page

To start, create a custom page in your help center for the integration. The page contains the UI that lets users access the integration. The page also includes client-side JavaScript that makes API requests to a local web server. The requests let the page load, add, and delete to-do items stored on the server.

To create the custom page

  1. In Guide, open the code editor for the theme and add a custom page named todo-list. For instructions, see Creating custom pages in Zendesk help.

  2. In code editor, copy and paste the following markup into the todo-list.hbs page.

    The page loads, adds, and deletes to-do items by making requests to a local web server at http://localhost:3000. These requests include a help center JWT as a bearer token in the Authorization header. The JWT is retrieved from the New Token endpoint and cached to help avoid rate limiting.

    The Authorization header and New Token request are highlighted.

    <div class="container">  <h1>To Do List</h1>  <div id="todo_app_content">    <input type="text" id="todo_item" />    <button onclick="addTodoItem()">Add Item</button>    <br /><br />    <ul id="todo_list">      <li>Loading items ...</li>    </ul>  </div></div>
    <script type="text/javascript">  const TODO_API_BASE_URL = "http://localhost:3000"
      const isUserAnonymous = () => window.HelpCenter.user.role === "anonymous"
      const showAppContent = () => {    isUserAnonymous() ? showSignInMessage() : loadTodoItems()  }
      const showSignInMessage = () => {    const signInMessage = "<p>You must be logged in to use this app.</p>"    document.getElementById("todo_app_content").innerHTML = signInMessage  }
      const loadTodoItems = async () => {    const jwt = await getJwt()    const todoItems = await todoRequest("/todo_items", "GET")    refreshTodoList(todoItems)  }
      const addTodoItem = async () => {    const todoItemInput = document.getElementById("todo_item")    const requestBody = { value: todoItemInput.value }    const todoItems = await todoRequest("/todo_items", "POST", requestBody)    refreshTodoList(todoItems)    todoItemInput.value = ""  }
      const deleteTodoItem = async todoItemId => {    const todoItems = await todoRequest(      `/todo_items/${todoItemId}`,      "DELETE"    )    refreshTodoList(todoItems)  }
      const todoRequest = async (endpoint, method, body) => {    const jwt = await getJwt()    const options = {      method: method,      headers: {        Authorization: `Bearer ${jwt}`,        "Content-Type": "application/json"      }    }    if (body) {      options.body = JSON.stringify(body)    }    const response = await fetch(`${TODO_API_BASE_URL}${endpoint}`, options)    const responseBody = await response.json()    return responseBody.todo_items  }
      const refreshTodoList = todoItems => {    const itemsList = todoItems      .map(        item =>          `<li>${item.value} <a aria-hidden="true" onclick="deleteTodoItem(${item.id})" style="cursor:pointer; color: red;">&#10006;</a>`      )      .join("")    document.getElementById("todo_list").innerHTML = itemsList  }
      const getJwt = async () =>    isTokenExpired(localStorage.jwt)      ? await refreshJwt()      : JSON.parse(localStorage.jwt).value
      const isTokenExpired = token =>    !token || Date.now() > JSON.parse(token).expiresAt
      const refreshJwt = async () => {    const { token } = await fetch("/api/v2/help_center/integration/token").then(      response => response.json()    )    setToken("jwt", token, 1)    return token  }
      const setToken = (key, value, ttl) => {    const data = { value, expiresAt: Date.now() + ttl * 1000 }    localStorage.setItem(key, JSON.stringify(data))  }
      window.addEventListener("load", showAppContent)</script>
  3. In code editor, click Publish.

Creating the web server

Next, create a local web server that receives PAI requests from your help center's custom page. In production, you'd also set up a database to store to-do items for each user. For this tutorial, you'll use a JSON file to store the data instead.

To create the web server

  1. In your terminal, create and navigate to a folder named hc_jwt_example. Example:

    mkdir hc_jwt_examplecd hc_jwt_example
  2. In the folder, use the following command to create an npm package.

    npm init -y
  3. Install dependencies for the project. This project requires the following libraries:

    • express.js: Sets up a basic web server
    • jsonwebtoken: Verifies and decodes JWTs
    • jwks-rsa: Constructs a signing key used to verify JWTs from a set of JSON web keys (JWKs). For help center JWTs, you retrieve this set from the List Public Keys endpoint
    • cors: Allows incoming CORS requests to the web server
    • body-parser: Allows the web server to parse the request body of incoming requests
    • node-json-db: Lets you use a JSON file as a database
    npm install express jsonwebtoken jwks-rsa cors body-parser node-json-db
  4. In the hc_jwt_example folder, create an app.js file. Paste the following code into the file.

    The code includes an authMiddleware function that does the following:

    • Extracts the help center JWT from the Authorization header of incoming requests
    • Retrieves JWK objects from the List Public Keys endpoint
    • Finds a matching JWK for the JWT using its kid
    • Gets a public signing key for the JWT's signature
    • Verifies the JWT's signature and expiration
    • Decode the JWT's payload
    • Includes the Zendesk user id from the JWT payload in the request's userId property

    The server also sets up API endpoints for fetching, adding, and deleting to-do items. The endpoints use the Zendesk user id from the JWT payload to scope requests to these endpoints to a specific user.

    const express = require("express")const jwt = require("jsonwebtoken")const jwksClient = require("jwks-rsa")const cors = require("cors")const bodyParser = require("body-parser")const { JsonDB, Config } = require("node-json-db")
    const app = express()const port = 3000
    const ZENDESK_SUBDOMAIN = "YOUR_ZENDESK_SUBDOMAIN"const DATABASE_FILE_NAME = "todoDatabase"const jwksClientInstance = jwksClient({  jwksUri: `https://${ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/help_center/integration/keys.json`})
    const authMiddleware = async (request, response, next) => {  try {    const token = getTokenFromAuthHeader(request)    const verifiedTokenPayload = await verifyToken(token)    request.userId = verifiedTokenPayload.userId    next()  } catch (err) {    console.error(err)    response.status(401).json({ message: "Invalid authentication" })  }}
    const getTokenFromAuthHeader = request => {  const authorization = request.headers?.authorization  if (!authorization) {    throw new Error("Authorization header missing")  }  return authorization.split(" ")[1]}
    const verifyToken = async token => {  const publicKey = await getPublicKeyFromJwks(    jwt.decode(token, { complete: true })  )  return jwt.verify(token, publicKey)}const getPublicKeyFromJwks = async decodedToken => {  const kid = decodedToken.header.kid  const signingKey = await jwksClientInstance.getSigningKey(kid)  return signingKey.rsaPublicKey}
    // In production, use a databaseconst db = new JsonDB(new Config(DATABASE_FILE_NAME, true, false, "/"))
    app.get("/todo_items", async (request, response) => {  const todoItems = await getUserTodoItems(request.userId)  response.json({ todo_items: todoItems })})
    app.post("/todo_items", async (request, response) => {  const todoItems = await getUserTodoItems(request.userId)  const newItemId = Math.floor(Date.now() * Math.random())  const newItem = { id: newItemId, value: request.body.value }  todoItems.push(newItem)  db.push(`/${request.userId}/todo_items`, todoItems)  response.json({ todo_items: todoItems })})
    app.delete("/todo_items/:itemId", async (request, response) => {  const todoItems = await getUserTodoItems(request.userId)  const itemIdToDelete = parseInt(request.params.itemId)  const filteredTodoItems = todoItems.filter(    item => item.id !== itemIdToDelete  )  db.push(`/${request.userId}/todo_items`, filteredTodoItems)  response.json({ todo_items: filteredTodoItems })})
    const getUserTodoItems = async userId => {  const dataPath = `/${userId}/todo_items`  const dataPathExists = await db.exists(dataPath)  return dataPathExists ? await db.getData(dataPath) : []}
    app.listen(port, () => {  console.log(    `Server running on port ${port}. Visit http://localhost:${port}`  )})

    In the code, replace the "YOUR_ZENDESK_SUBDOMAIN" placeholder with your Zendesk subdomain.

  5. Start a local web server using the app. Run the following terminal command from the hc_jwt_example folder:

    node app.js

    To stop the server, press Ctrl+C.

Testing the integration

To finish, test the integration to ensure users can manage to-do items as intended. To use the app, you must be signed in to Zendesk.

  1. Use your app to start a local web server.

  2. In a web browser, navigate to the custom page you added in Creating the custom page. The URL should look like this:


    Replace {subdomain} with your Zendesk subdomain.

  3. Use the custom page to add and remove to-do items.

  4. Repeat the process using different Zendesk users.

    Ensure that to-do items aren't shared across different Zendesk users. Also ensure that to-do items persist for each user between sessions.