OpenPhone Webhooks in Next.js: Building a Smart Communication Hub

OpenPhone Webhooks in Next.js: Building a Smart Communication Hub

Deniz Birlik
Deniz Birlik
·10 min read

In today's digital-first business world, smooth communication isn't just nice to have—it's a must. OpenPhone, a modern business phone system, offers powerful webhook features that can change how you handle calls and messages. This guide will walk you through integrating OpenPhone webhooks with your Next.js application, helping you create a smart, responsive communication hub that fits your business needs.

Understanding OpenPhone Webhooks: The Key to Real-Time Communication

OpenPhone webhooks act like real-time messengers, giving you instant updates about various communication events. By using these webhooks, you can:

  • Automatically log calls in your CRM system
  • Trigger smart responses to incoming messages
  • Generate useful insights about communication patterns
  • Smoothly integrate voice and text communications with your existing workflows

Let's dive in and boost your communication game with Next.js and OpenPhone!

Setting Up Your Next.js Project: The Foundation of Your Communication Hub

If you already have a Next.js project ready, feel free to skip to the webhook API route creation.

For those starting from scratch, let's set up the basics for our communication hub:

npx create-next-app@latest openphone-webhook-hub
cd openphone-webhook-hub

When asked, choose these options:

  • Use TypeScript for better code quality
  • Use ESLint to catch errors
  • Use Tailwind CSS for easy styling
  • Use the src/ directory for better organization
  • Use the App Router for improved routing
  • Stick with the default import alias

Creating the Webhook API Route: Your Communication Command Center

In Next.js, API routes are perfect for handling OpenPhone webhook events. Create a new file at src/app/api/webhooks/openphone/route.ts and add this code:

import { NextResponse } from 'next/server';
import { verifyWebhookSignature } from '@/lib/openphone';
import { handleCallCompleted, handleMessageReceived } from '@/lib/handlers';

export async function POST(request: Request) {
  const body = await request.json();
  const signature = request.headers.get('openphone-signature');

  if (!signature || !verifyWebhookSignature(signature, JSON.stringify(body))) {
    console.warn('Invalid OpenPhone webhook signature');
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const eventType = body.type;

  console.info(`Received OpenPhone webhook: ${eventType}`, body);

  switch (eventType) {
    case 'call.completed':
      return handleCallCompleted(body.data.object);
    case 'message.received':
      return handleMessageReceived(body.data.object);
    default:
      console.warn(`Unhandled OpenPhone event: ${eventType}`);
      return NextResponse.json({ status: 'unhandled event' }, { status: 200 });
  }
}

This API route acts as the main hub for all incoming OpenPhone webhook events, sending them to the right handlers based on the event type.

Implementing Solid Webhook Security: Trust, but Verify

Security is crucial when dealing with external webhooks. OpenPhone uses a strong method to secure its webhooks, and we'll implement this verification carefully.

Create a new file at src/lib/openphone.ts with this content:

import crypto from 'crypto';

export function verifyWebhookSignature(signature: string, payload: string): boolean {
  const signingSecret = process.env.OPENPHONE_SIGNING_SECRET;

  if (!signature || !signingSecret) {
    return false;
  }

  const fields = signature.split(';');
  const timestamp = fields[2];
  const providedDigest = fields[3];

  const signedData = `${timestamp}.${payload}`;

  const signingKeyBinary = Buffer.from(signingSecret, 'base64');

  const computedDigest = crypto
    .createHmac('sha256', signingKeyBinary)
    .update(signedData)
    .digest('base64');

  return crypto.timingSafeEqual(Buffer.from(providedDigest), Buffer.from(computedDigest));
}

Don't forget to update your .env.local file with the signing secret from your OpenPhone account:

OPENPHONE_SIGNING_SECRET=your_signing_secret_here

Handling Specific Events: Turning Data into Action

Now, let's create handlers for the call.completed and message.received events. Create a new file at src/lib/handlers.ts:

import { NextResponse } from 'next/server';

interface CallData {
  id: string;
  from: string;
  to: string;
  createdAt: string;
  completedAt?: string;
  voicemail?: {
    url: string;
  };
  direction: 'incoming' | 'outgoing';
}

interface MessageData {
  id: string;
  from: string;
  to: string;
  body: string;
  media?: Array<{
    url: string;
    type: string;
  }>;
}

export async function handleCallCompleted(callData: CallData) {
  const { id, from, to, createdAt, completedAt, voicemail, direction } = callData;

  const duration = completedAt
    ? (new Date(completedAt).getTime() - new Date(createdAt).getTime()) / 1000
    : null;

  console.info(`Call completed: ${id} from ${from} to ${to}, duration: ${duration} seconds`);

  if (voicemail) {
    console.info(`Voicemail received: ${voicemail.url}`);
    // Here you could transcribe the voicemail and notify the user
    // await transcribeAndNotify(voicemail.url);
  }

  // Update your CRM or internal systems
  // await logCallInCRM(from, to, duration, direction);

  return NextResponse.json({ status: 'call completed handled' }, { status: 200 });
}

export async function handleMessageReceived(messageData: MessageData) {
  const { id, from, to, body, media } = messageData;

  console.info(`Message received: ${id} from ${from} to ${to}`);

  if (media && media.length > 0) {
    media.forEach(attachment => {
      console.info(`Media attachment: ${attachment.url} (${attachment.type})`);
      // Process or store the media
      // await processAttachment(attachment.url, attachment.type);
    });
  }

  // Trigger an automated response if needed
  // await sendAutomatedReply(to, from, body);

  return NextResponse.json({ status: 'message received handled' }, { status: 200 });
}

Testing Your Integration

To test your webhook integration, you can use Webhook Simulator:

  1. Sign up for a Webhook Simulator account, visit "Platforms" page and add OpenPhone to your library.

  2. Install the Webhook Simulator CLI tool. You can find installation instructions in the documentation.

  3. Start the Webhook Simulator CLI to listen for webhook events and forward them to your local Laravel application:

ws-cli listen --forward-to http://localhost:3000/webhooks/openphone

Make sure to replace http://localhost:3000/webhooks/openphone with the actual URL where your Laravel application is listening for webhooks.

  1. Now you have two options for simulating OpenPhone webhook events:

a. Use the Webhook Simulator web application to create and send custom payloads.

b. Use the CLI to trigger predefined events. Open a new terminal window and run:

ws-cli trigger openphone call.completed

You can replace call.completed with any other OpenPhone event you wish to test.

  1. The simulated webhook will be sent to your local Laravel application via the forwarding set up in step 3.

  2. Check your Laravel logs to see how your application handled the simulated webhook event.

Here's an example of what an OpenPhone call.completed event payload might look like (note that this is a simplified version):

{
    "id": "EVbf9e2f2a8b0b4a71a76f46d4f3a78f1f",
    "data": {
        "object": {
            "id": "AC5f7d2a1b4a0b4b2a8c7d2e1f3a4b5c6",
            "to": "+16665550456",
            "from": "+15555550123",
            "media": [],
            "object": "call",
            "status": "completed",
            "userId": "USb1c2d3e4f",
            "createdAt": "2022-02-01T12:33:45.678Z",
            "direction": "incoming",
            "voicemail": {
                "url": "https://m.openph.one/static/b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5.mp3",
                "type": "audio/mpeg",
                "duration": 10
            },
            "answeredAt": null,
            "completedAt": "2022-02-01T12:34:01.000Z",
            "phoneNumberId": "PNf5a6b7c8d",
            "conversationId": "CNe9a8b7c6d5"
        }
    },
    "type": "call.completed",
    "object": "event",
    "createdAt": "2022-02-01T12:34:56.789Z",
    "apiVersion": "v2"
}

Advanced Webhook Handling Techniques

Now that we've covered the basics, let's dive into some more advanced techniques to make your OpenPhone webhook integration even more powerful.

1. Implementing Retry Logic

Sometimes, webhook deliveries might fail due to temporary issues. Implementing a retry mechanism can help ensure you don't miss important events.

import { NextResponse } from 'next/server';

async function processWebhook(body: any) {
  const maxRetries = 3;
  let retries = 0;

  while (retries < maxRetries) {
    try {
      // Your webhook processing logic here
      return NextResponse.json({ status: 'success' }, { status: 200 });
    } catch (error) {
      console.error(`Webhook processing failed. Retry ${retries + 1}/${maxRetries}`);
      retries++;
      if (retries === maxRetries) {
        throw error;
      }
      // Wait for a short time before retrying
      await new Promise(resolve => setTimeout(resolve, 1000 * retries));
    }
  }
}

export async function POST(request: Request) {
  const body = await request.json();
  return processWebhook(body);
}

2. Handling Rate Limits

If you're processing a large volume of webhooks, you might need to implement rate limiting to avoid overwhelming your systems or third-party APIs you're using.

import { NextResponse } from 'next/server';
import { RateLimiter } from 'limiter';

const limiter = new RateLimiter({ tokensPerInterval: 10, interval: 'second' });

export async function POST(request: Request) {
  if (!await limiter.removeTokens(1)) {
    return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
  }

  // Process the webhook
  // ...
}

3. Webhook Event Logging

Keeping a log of webhook events can be crucial for debugging and auditing purposes. Here's a simple implementation using a database (you'll need to set up your database connection):

import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function POST(request: Request) {
  const body = await request.json();

  try {
    await prisma.webhookEvent.create({
      data: {
        eventType: body.type,
        payload: JSON.stringify(body),
        processedAt: new Date(),
      },
    });

    // Process the webhook
    // ...

    return NextResponse.json({ status: 'success' }, { status: 200 });
  } catch (error) {
    console.error('Failed to log webhook event', error);
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

4. Implementing Webhooks for Different Environments

In a real-world scenario, you'll likely have different environments (development, staging, production). Here's how you can handle webhooks for different environments:

import { NextResponse } from 'next/server';

const ENVIRONMENT = process.env.NODE_ENV || 'development';

export async function POST(request: Request) {
  const body = await request.json();

  console.log(`Processing webhook in ${ENVIRONMENT} environment`);

  // Environment-specific logic
  switch (ENVIRONMENT) {
    case 'development':
      // Maybe log everything and skip actual processing
      console.log('Webhook payload:', JSON.stringify(body, null, 2));
      return NextResponse.json({ status: 'logged' }, { status: 200 });
    case 'staging':
      // Perhaps do everything except sending actual communications
      // ...
      break;
    case 'production':
      // Full processing
      // ...
      break;
  }

  // Common processing logic
  // ...

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

Integrating with Your Business Logic

Now that we've covered some advanced handling techniques, let's look at how you can integrate OpenPhone webhooks with your business logic to create truly smart workflows.

1. Automatic Task Creation

When a voicemail is received, you might want to automatically create a task in your project management system:

import { NextResponse } from 'next/server';
import { createTask } from '@/lib/taskManager';

export async function handleCallCompleted(callData: CallData) {
  if (callData.voicemail) {
    await createTask({
      title: `Follow up on voicemail from ${callData.from}`,
      description: `Voicemail received at ${callData.completedAt}. Listen here: ${callData.voicemail.url}`,
      dueDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Due in 24 hours
    });
  }

  // Rest of call handling logic
  // ...

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

2. Smart Auto-Responder

For incoming messages, you could implement a smart auto-responder that uses AI to generate appropriate responses:

import { NextResponse } from 'next/server';
import { generateAIResponse } from '@/lib/ai';
import { sendMessage } from '@/lib/openphone';

export async function handleMessageReceived(messageData: MessageData) {
  const aiResponse = await generateAIResponse(messageData.body);

  if (aiResponse) {
    await sendMessage(messageData.from, aiResponse);
  }

  // Rest of message handling logic
  // ...

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

3. Call Analytics

You can use webhook data to build detailed call analytics:

import { NextResponse } from 'next/server';
import { updateCallStats } from '@/lib/analytics';

export async function handleCallCompleted(callData: CallData) {
  const duration = callData.completedAt
    ? (new Date(callData.completedAt).getTime() - new Date(callData.createdAt).getTime()) / 1000
    : 0;

  await updateCallStats({
    date: new Date(callData.createdAt),
    duration,
    direction: callData.direction,
    hasVoicemail: !!callData.voicemail,
  });

  // Rest of call handling logic
  // ...

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

Getting Ready for Production: Scaling Your Communication Hub

As you prepare to launch your OpenPhone webhook integration, consider these important optimizations:

  1. Use a Job Queue: For tasks that take a lot of time or resources, use a job queue system. This keeps your webhook endpoint quick and responsive, even when it's busy.

  2. Make It Idempotent: Design your handlers to safely process the same event multiple times. This protects against potential issues with duplicate webhook deliveries.

  3. Set Up Good Monitoring: Use a service like Sentry for tracking errors and logging. This gives you valuable insights into how your system is performing and any issues that come up.

  4. Use Scalable Infrastructure: Choose a hosting solution that can automatically handle varying amounts of webhook traffic. Platforms like Vercel work great for this.

  5. Keep Your Config Secure: Always store sensitive information, like the OpenPhone signing secret, in environment variables. Never put these values directly in your code.

Conclusion

By implementing these advanced techniques and integrating OpenPhone webhooks deeply with your business logic, you're creating a truly smart communication system. This system doesn't just handle calls and messages; it turns every interaction into an opportunity to streamline your workflows, enhance customer service, and gain valuable insights.

Remember, the key to a successful webhook integration is continuous refinement. Keep an eye on your logs, gather feedback from your team, and don't be afraid to adjust your implementation as you learn more about your communication patterns and needs.

With Next.js and OpenPhone webhooks, you're well-equipped to build a communication hub that's not just reactive, but proactive — anticipating needs, streamlining tasks, and ultimately driving your business forward. Happy coding, and here's to smarter, more efficient communication!