Adding a diagnostic WhatsApp status endpoint to a Next.js API (app/api/whatsapp/estado/route.ts)
TL;DR: I added a new admin‑only endpoint (/api/whatsapp/estado) that returns the health of our WhatsApp Business API client and automatically subscribes the webhook if needed. The change also fixed a routing conflict that was causing every /api/whatsapp/* request to return 404.
The Problem
Our ticketing platform albaida‑tickets relies on a WhatsApp Business API client to receive voice notes from residents. The client runs in production behind a webhook that forwards incoming messages to our backend. After a weekend of silent tickets, the admin console showed an empty “WhatsApp status” widget, and the logs only contained:
Error: GET https://graph.facebook.com/v17.0/<WHATSAPP_ID>/messages?access_token=***
^^^
404 Not Found
Two things were wrong:
- The admin UI had no way to query the WhatsApp client health.
- All API routes under
pages/api/whatsapp/*were returning 404, even though the files existed.
The root cause was a routing bug introduced when we mixed the Pages Router (pages/api/...) with the App Router (app/api/...) in a Next.js 13 project. The Pages Router took precedence and swallowed any request that matched the folder, but we had no index.ts there, so the request fell through to a 404.
What I Tried First
My first attempt was to add a quick‑and‑dirty file under the old Pages Router:
pages/api/whatsapp/status.ts
// pages/api/whatsapp/status.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getWhatsAppHealth } from '../../../lib/whatsapp';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') return res.status(405).end();
try {
const health = await getWhatsAppHealth();
res.status(200).json(health);
} catch (e) {
res.status(500).json({ error: (e as Error).message });
}
}
I hit the endpoint (/api/whatsapp/status) and got a 404. The file was correctly placed, but the request never reached it. I inspected the Next.js routing table and realized that any folder named whatsapp under pages/api was being shadowed by a catch‑all dynamic route we had in pages/api/[...slug].ts for legacy integrations. The catch‑all was evaluated first, returning a 404 for unknown slugs.
I tried moving the catch‑all to a lower priority (pages/api/legacy/[...slug].ts) and adding a custom rewrites entry in next.config.js, but that introduced a new set of edge‑case bugs where other third‑party integrations started failing.
The Implementation
The clean solution was to switch the new endpoint to the App Router, which isolates it from the legacy Pages Router and gives us built‑in support for route handlers, middlewares, and dynamic metadata.
1. File structure
app/
└─ api/
└─ whatsapp/
└─ estado/
└─ route.ts ← new endpoint
2. The route handler (app/api/whatsapp/estado/route.ts)
// app/api/whatsapp/estado/route.ts
import { NextResponse } from 'next/server';
import { getWhatsAppHealth, ensureWebhook } from '@/lib/whatsapp';
import { adminAuth } from '@/middleware/adminAuth';
// Force dynamic rendering – we always want fresh data
export const dynamic = 'force-dynamic';
// Only allow GET requests
export async function GET(request: Request) {
// 1️⃣ Authenticate the admin user
const authResult = await adminAuth(request);
if (!authResult.ok) {
return new NextResponse(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
}
try {
// 2️⃣ Query the WhatsApp Business API health endpoint
const health = await getWhatsAppHealth();
// 3️⃣ If the webhook is not registered, try to subscribe automatically
if (!health.webhookRegistered) {
await ensureWebhook();
}
// 4️⃣ Return a compact JSON payload
return NextResponse.json({
status: health.status,
webhookRegistered: health.webhookRegistered,
lastPing: health.lastPing,
});
} catch (err) {
console.error('WhatsApp status error:', err);
return new NextResponse(
JSON.stringify({ error: (err as Error).message }),
{ status: 500 }
);
}
}
Key points
| Decision | Reason |
|---|---|
App Router (app/api/...) |
Isolates new routes from legacy catch‑alls, eliminates 404 shadowing. |
export const dynamic = 'force-dynamic' |
Guarantees fresh health data on each request; we never want stale cache. |
adminAuth middleware |
Keeps the endpoint internal – only admins can query WhatsApp health. |
ensureWebhook() |
Auto‑subscribes the webhook if it was removed (e.g., after a token rotation). |
NextResponse.json |
Uses the built‑in response helper for correct Content-Type and serialization. |
3. Helper functions (lib/whatsapp.ts)
ts
// lib/
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/content-automation` · 2026-10-24*
\#playadev #buildinpublic
Top comments (0)