Logo
Search
API Docs

Next.js Server Integration

Framework Integrations

Next.js Server Integration: Webhook Route Handlers for the App Router & Pages Router

Overview

If your assistant's server URL points at a Next.js application, you can handle webhook events with either the App Router (a Route Handler) or the Pages Router (an API Route). Both approaches receive the same JSON event payload from the core system and follow the same core pattern: read the event's type field, switch on it, and respond with the correct HTTP status code.

This page walks through a worked handler for both routers, the event types you'll commonly need to handle, the response rules the core system expects, and how to test your handler against real events during local development.


App Router: Route Handler

Create a Route Handler at app/api/webhook/route.ts:

import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const data = await req.json();
  const { type, call, timestamp } = data;

  console.log(`Webhook received: ${type} at ${timestamp}`);

  switch (type) {
    case 'call-started':
      console.log(`Call ${call.id} started with ${call.customer?.number}`);
      break;

    case 'speech-update':
      console.log(`User said: ${data.transcript}`);
      break;

    case 'function-call': {
      const { functionName, parameters } = data.functionCall;
      const result = await processFunction(functionName, parameters);
      return NextResponse.json({ result });
    }

    case 'assistant-request': {
      // Return a transient assistant configuration or reject the call
      return NextResponse.json({
        assistant: {
          name: 'Inbound Receptionist',
          firstMessage: 'Hi there! How can I help you today?'
        }
      });
    }

    case 'call-ended':
      console.log(`Call ended. Duration: ${call.duration}s`);
      break;

    default:
      console.log(`Unhandled event type: ${type}`);
  }

  return NextResponse.json({ status: 'ok' }, { status: 200 });
}

async function processFunction(name: string, params: unknown) {
  // Your custom function logic here
  return { success: true };
}

Pages Router: API Route

If your project uses the Pages Router instead, create the handler at pages/api/webhook.ts:

import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { type, call, timestamp } = req.body;

  console.log(`Webhook received: ${type} at ${timestamp}`);

  switch (type) {
    case 'call-started':
      console.log(`Call ${call.id} started with ${call.customer?.number}`);
      break;

    case 'speech-update':
      console.log(`User said: ${req.body.transcript}`);
      break;

    case 'function-call': {
      const { functionName, parameters } = req.body.functionCall;
      const result = await processFunction(functionName, parameters);
      return res.status(200).json({ result });
    }

    case 'call-ended':
      console.log(`Call ended. Duration: ${call.duration}s`);
      break;

    default:
      console.log(`Unhandled event type: ${type}`);
  }

  return res.status(200).json({ status: 'ok' });
}

async function processFunction(name: string, params: unknown) {
  // Your custom function logic here
  return { success: true };
}

Both routers follow the same shape: parse the body, switch on type, and return JSON with a result for function-call events (and an assistant object for assistant-request events) rather than a bare status code.


Handling Event Types & Response Codes

The most common event types you'll switch on are:

CategoryEvent types
Call lifecyclecall-started, call-ended, call-failed
Speechspeech-update, transcript
Assistantfunction-call, assistant-request, conversation-update

For the full event catalog, see Webhooks & Events.

Your route handler's HTTP status code determines what happens next:

Status CodeMeaning
200–299Success, event processed
400–499Client error, event rejected (not retried)
500–599Server error (will be retried)

For the full retry and timeout mechanics behind these codes, see Webhook Delivery: Retries, Timeouts & Debugging.


Testing Locally

To exercise your Next.js handler against real events during development, pair the core system's CLI listener with a tunneling service such as ngrok:

# Terminal 1: Expose a local port via a tunneling service
ngrok http 4242

# Terminal 2: Forward webhooks to your Next.js dev server
core-system listen --forward-to localhost:3000/api/webhook

Update your Sulus dashboard webhook URL to the tunnel's public URL. The data flow is: core system > tunnel > core-system listen (port 4242) > your Next.js server. Note that core-system listen is a forwarder only and does not create a public URL itself — you still need the separate tunneling service.

For CLI authentication across multiple accounts or key rotation during this workflow, see CLI Key Rotation & Multi-Account.