· 4 min · TextMeFlow Team

Send WhatsApp Messages from Next.js API Routes on Vercel

Next.js on Vercel is a common place to wire up WhatsApp notifications: a form submission, an order confirmation, a support ticket update. The tricky part isn't the HTTP call — it's making that call reliably from a serverless function, and receiving webhooks back on a platform with no persistent server. This guide covers both.

Sending a message from an API route

A Route Handler (the app/api/*/route.ts convention) is the simplest place to call the textmeflow API. It runs as a short-lived serverless function, so keep the request synchronous and return a response once the API call settles — don't fire-and-forget without awaiting it, or Vercel can freeze the function before the request completes.

// app/api/notify/route.ts
export async function POST(req: Request) {
  const { phone, message } = await req.json();

  const res = await fetch('https://api.textmeflow.eu/v1/messages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TEXTMEFLOW_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ to: phone, text: message }),
  });

  if (!res.ok) {
    const error = await res.json();
    return Response.json({ error }, { status: res.status });
  }

  return Response.json({ sent: true });
}

Store TEXTMEFLOW_API_KEY as a Vercel environment variable, not in code — it should never be exposed to the client bundle, so only reference it inside a Route Handler or Server Action, never in a 'use client' component.

Calling it from a Server Action

If you're already on the App Router, a Server Action skips the extra network hop of calling your own API route from the client:

// app/actions.ts
'use server';

export async function sendConfirmation(phone: string, orderId: string) {
  const res = await fetch('https://api.textmeflow.eu/v1/messages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TEXTMEFLOW_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      text: `Your order ${orderId} is confirmed.`,
    }),
  });

  return res.ok;
}

Call this directly from a form's action prop or after a mutation completes — no client-side fetch, no API route to maintain.

Receiving webhooks on Vercel

If you also want delivery status or inbound replies, you need a Route Handler that accepts POST requests from textmeflow and verifies the signature before trusting the payload. Every webhook request carries an HMAC signature — see the webhook docs for the exact header and verification steps. Skipping that check means anyone who finds your endpoint URL can post fake delivery events.

// app/api/webhooks/textmeflow/route.ts
import crypto from 'crypto';

export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get('x-textmeflow-signature') ?? '';

  const expected = crypto
    .createHmac('sha256', process.env.TEXTMEFLOW_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex');

  if (signature !== expected) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody);
  // handle event.type: message.delivered, message.failed, message.received, ...

  return new Response('ok', { status: 200 });
}

Two serverless-specific details matter here. First, don't use req.json() before verifying the signature — you need the exact raw bytes textmeflow signed, and re-serializing a parsed object can produce a different byte string that fails verification. Second, Vercel functions have a cold-start and execution-time budget: keep webhook handling fast (log, queue, acknowledge) and push slow work like sending a follow-up message or writing to a database to a background job or queue, rather than blocking the response.

Idempotency on retries

textmeflow retries a webhook delivery if your endpoint doesn't respond in time, which on a cold-starting serverless function happens more often than on a warm server. Each event carries an event_id — store processed IDs (even just in a KV store or your database) and skip duplicates, so a retried message.delivered event doesn't trigger a duplicate notification downstream. The webhook retry and idempotency guide covers the pattern in more detail.

Getting started

You only need an API key to start sending: pair your WhatsApp number through the QR flow, grab the key from the dashboard, and the Route Handler above is a working integration in a few minutes. See the getting started guide for the pairing step, then try textmeflow free — the free plan includes 50 messages a month to test the whole flow before you commit to anything.

Zelf WhatsApp-berichten versturen via API?

Gratis voor altijd tot 50 berichten/maand. QR scannen en binnen 5 minuten verstuur je je eerste bericht.

Gratis voor altijd