You've built a powerful AI agent as a Next.js API route. You tested it with curl using a POST request — works perfectly. You add it to vercel.json as a cron job, deploy to production, and wait. The scheduled time comes and goes. Nothing happens. No database entries, no emails, no generated images.
When you check your Vercel logs, you don't see any application errors. Instead, buried in the raw HTTP traffic, you spot this tiny, easily-missed log:
GET 405 /api/your-cron-endpoint
The Root Cause
- Next.js App Router is strict: If you only export export async function POST, Next.js rejects any other HTTP method with a 405 Method Not Allowed — immediately, at the routing level, before your code runs.
- Vercel Cron defaults to GET: Unless told otherwise, Vercel's scheduler invokes cron endpoints via HTTP GET requests.
The cron fires, Next.js rejects the GET silently, no application error is thrown, and nothing in your logs points to the real problem.
The Fix
Add a GET alias in your route file that delegates to POST:
export async function POST(request) {
// your cron logic
return NextResponse.json({ status: 'success' });
}
export async function GET(request) {
return POST(request);
}
Security Warning
Opening a GET route means anyone can trigger it from a browser. Always verify Vercel's x-vercel-cron-auth-token header before executing expensive operations:
export async function POST(request) {
const isVercelCron = request.headers.get('x-vercel-cron-auth-token') ||
request.headers.get('user-agent')?.includes('vercel-cron');
const authHeader = request.headers.get('authorization');
if (!isVercelCron && authHeader !== `Bearer ${process.env.AGENT_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// safe to run
}
Summary
- Check Vercel traffic logs for 405 errors on your cron path.
- Add export async function GET(request) { return POST(request); } to your route.
- Secure the endpoint with Vercel's cron auth headers.