> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layercode.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Get started with ExpressJS

> Implement the Layercode Webhook SSE API in your ExpressJS backend.

This guide shows you how to implement the Layercode [Webhook SSE API](/api-reference/webhook-sse-api) in an [ExpressJS](https://expressjs.com/) backend. You'll learn how to set up a webhook endpoint that receives transcribed messages from the Layercode voice agent and streams the agent's responses back to the frontend, to be turned into speech and spoken back to the user. You can test your backend using the Layercode dashboard playground or by following the [Build a Web Voice Agent](/tutorials/next-js-frontend) guide.

**Example code:** <Icon icon="github" size={18} color="black" /> [layercodedev/example-backend-express](https://github.com/layercodedev/example-backend-express)

## Prerequisites

* Node.js 18+
* [Express](https://expressjs.com/)
* A Layercode account and agent ([sign up here](https://dash.layercode.com))
* (Optional) An API key for your LLM provider (we recommend Google Gemini)

## Setup

```bash theme={null}
npm install express @layercode/node-server-sdk ai @ai-sdk/google node-fetch
```

Edit your .env environment variables. You'll need to add:

* `GOOGLE_GENERATIVE_AI_API_KEY` - Your Google AI API key
* `LAYERCODE_WEBHOOK_SECRET` - Your Layercode agent's webhook secret, found in the [Layercode dashboard](https://dash.layercode.com) (go to your agent, click Edit in the Your Backend Box and copy the webhook secret shown)
* `LAYERCODE_API_KEY` - Your Layercode API key found in the [Layercode dashboard settings](https://dash.layercode.com/settings)

## Create Your Express Webhook Endpoint

Here's an example of a our Layercode webhook endpoint, which generates responses using Google Gemini and streams them back to the frontend as SSE events. See the [GitHub repo](https://github.com/layercodedev/example-backend-express) for the full example.

<CodeGroup>
  ```ts agent.ts [expandable] theme={null}
  import type { RequestHandler } from 'express';
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
  import { streamText } from 'ai';
  import type { CoreMessage } from 'ai';
  import { verifySignature, streamResponse } from '@layercode/node-server-sdk';
  import { Readable } from 'node:stream'; // Node.js 18+ only

  const google = createGoogleGenerativeAI({
    apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
  });

  const sessionMessages: Record<string, CoreMessage[]> = {};

  const SYSTEM_PROMPT =
    "You are a helpful conversation assistant. You should respond to the user's message in a conversational manner. Your output will be spoken by a TTS model. You should respond in a way that is easy for the TTS model to speak and sound natural.";
  const WELCOME_MESSAGE = 'Welcome to Layercode. How can I help you today?';

  export const onRequestPost: RequestHandler = async (req, res) => {
    const requestBody = req.body;
    const signature = req.header('layercode-signature') || '';
    const secret = process.env.LAYERCODE_WEBHOOK_SECRET || '';
    const payload = JSON.stringify(requestBody);
    const isValid = verifySignature({ payload, signature, secret });
    if (!isValid) {
      console.error('Invalid signature', signature, secret, payload);
      res.status(401).send('Unauthorized');
      return;
    }
    console.log('Request body received from Layercode', requestBody);
    const { session_id, text, type } = requestBody;

    let messages = sessionMessages[session_id] || [];
    messages.push({ role: 'user', content: [{ type: 'text', text }] });

    let response;
    if (type === 'session.start') {
      response = streamResponse(requestBody, async ({ stream }: { stream: any }) => {
        stream.tts(WELCOME_MESSAGE);
        messages.push({
          role: 'assistant',
          content: [{ type: 'text', text: WELCOME_MESSAGE }],
        });
        stream.end();
      });
    } else {
      response = streamResponse(requestBody, async ({ stream }: { stream: any }) => {
        try {
          const { textStream } = streamText({
            model: google('gemini-2.0-flash-001'),
            system: SYSTEM_PROMPT,
            messages,
            onFinish: async ({ response }: { response: any }) => {
              messages.push(...response.messages);
              console.log('Current message history for session', session_id, JSON.stringify(messages, null, 2));
              sessionMessages[session_id] = messages;
            },
          });
          stream.data({
            textToBeShown: 'Hello, how can I help you today?',
          });
          await stream.ttsTextStream(textStream);
        } catch (err) {
          console.error('Handler error:', err);
        } finally {
          console.log('Stream ended');
          stream.end();
        }
      });
    }

    // Set headers and status
    response.headers.forEach((value, key) => res.setHeader(key, value));
    res.status(response.status);

    if (response.body) {
      const nodeStream = Readable.fromWeb(response.body as any);
      nodeStream.pipe(res);
    } else {
      res.end();
    }
  };
  ```

  ```ts authorize.ts theme={null}
  import type { RequestHandler } from 'express';

  export const onRequestPost: RequestHandler = async (req, res) => {
    try {
      const response = await fetch("https://api.layercode.com/v1/agents/web/authorize_session", {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${process.env.LAYERCODE_API_KEY}`,
        },
        body: JSON.stringify({ agent_id: "your-agent-id", conversation_id: null }),
      });
      if (!response.ok) {
        console.log('response not ok', response.statusText);
        res.json({ error: response.statusText });
      }
      const data: { client_session_key: string } = await response.json();
      res.json(data);
    } catch (error) {
      res.json({ error: error });
    }
  };
  ```

  ```ts index.ts theme={null}
  import express from 'express';
  import { onRequestPost as onRequestAgent } from './agent';
  import { onRequestPost as onRequestAuthorize } from './authorize';
  import cors from 'cors';

  const app = express();
  app.use(express.json());
  app.use(cors());
  app.post('/agent', onRequestAgent);
  app.post('/authorize', onRequestAuthorize);
  const PORT = process.env.PORT || 3001;
  app.listen(PORT, () => {
    console.log(`Express server listening on port ${PORT}`);
  })
  ```
</CodeGroup>

## 3. How It Works

* **/agent endpoint:** Receives POST requests from Layercode with the user's transcribed message, session, and turn info.
* **Session management:** Keeps track of conversation history per session (in-memory for demo; use a store for production).
* **LLM call:** Calls Google Gemini (or your own agent) with the conversation history and streams the response.
* **SSE streaming:** Streams the agent's response back to Layercode as Server-Sent Events, which are then converted to speech and played to the user.
* **/authorize endpoint:** Your Layercode API key should never be exposed to the frontend. Instead, your backend acts as a secure proxy: it receives the frontend's request then, calls the Layercode authorization API using your secret API key, and finally returns the `client_session_key` (and optionally a `conversation_id`) to the frontend. This key is required for the frontend to establish a secure WebSocket connection to Layercode.

## Running Your Backend

Start your Express server:

```bash theme={null}
npx tsx index.ts
```

## Configure the Layercode Webhook endpoint

In the Layercode dashboard, go to your agent settings. Under **Your Backend**, click edit, and here you can set the URL of the webhook endpoint.

If running this example locally, setup a tunnel (we recommend cloudflared which is free for dev) to your localhost so the Layercode webhook can reach your backend. Follow our [tunnelling guide](/how-tos/tunnelling).
