Webhooks/APIs don't work with Vercel

I am trying to deploy my web app but I keep getting errors 405, 500 etc. I also noticed that Stripe webhook links and other APIs under apis/webhook/* folder gives a 405 method not allowed error. How do I fix this and this is very frustrating. I tried numerous solutions online including CORS and they don’t work. The API endpoints were working on the *.vercel.app domain but not on my custom domain…

I am expecting for webhook events and API calls to be successful.

Give me a solution, my website is fhotosonic.com and I am using Node.js and Next.js framework.

The domain troubleshooting guide can help with most custom domain configuration issues. You might be able to use that guide to solve it before a human is available to help you. Then you can come back here and share the answer for bonus points.

You can also use v0 to narrow down the possibilities.

Hi @justinmky, welcome to the Vercel Community!

Sorry that you’re facing this issue.

Can you confirm whether the endpoint in the api/webhook/ folder are POST? Because this is the method that Stripe expects.

If you need more help, please share your public repo or a minimal reproducible example. That will let us all work together from the same code to figure out what’s going wrong.

How do I confirm this?

I believe Anshuman meant that the webhook handler file in that folder should be configured to accept POST requests. Could you share a minimal reproducible setup of your code that can help ensure we are all on the same page?

1 Like

@aruns @anshumanb

Here’s the code for both /api/webhooks/stripe and stripe.ts, It worked fine on vercel.app domain (which I unfortunately deleted after connecting custom domain) but doesn’t work on my custom domain.

The error I keep getting is 405 error “Cannot connect to remote host” when viewing events in Stripe dashboard…

src/lib/stripe.ts

import Stripe from "stripe"
import { env } from "@/env.mjs"
import { prisma } from "@/lib/db"

export const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
  apiVersion: "2024-04-10",
  typescript: true,
})

// commonly used Stripe related functions
export async function createCheckoutSession(
  amount: number,
  quantity: number,
  description: string,
  userId: string,
  emailAddress: string,
  metadata?: Record<string, string>
) {
  // Verify user exists first
  const user = await prisma.user.findUnique({
    where: { id: userId },
  });

  if (!user) {
    throw new Error("User not found");
  }

  const isTeamPurchase = metadata?.type === "TEAM";
  const successUrl = isTeamPurchase
    ? `${env.NEXT_PUBLIC_APP_URL}/dashboard/create/team/verify?session_id={CHECKOUT_SESSION_ID}`
    : `${env.NEXT_PUBLIC_APP_URL}/dashboard/create/individual/customize?session_id={CHECKOUT_SESSION_ID}`;

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price_data: {
          currency: 'usd',
          product_data: {
            name: `${quantity} Credits - fhotosonic.com`,
            description: description,
          },
          unit_amount: Math.round(amount * 100), // ensure amount is an integer
        },
        quantity: 1,
      },
    ],
    mode: 'payment',
    success_url: successUrl,
    cancel_url: `${env.NEXT_PUBLIC_APP_URL}/pricing`,
    metadata: {
      userId,
      credits: quantity.toString(),
      ...metadata,
    },
    customer_email: emailAddress,
    allow_promotion_codes: true,
  });

  // Create a StripeTransaction record
  await prisma.stripeTransaction.create({
    data: {
      userId,
      stripeSessionId: session.id,
      amount: amount * 100, // Store amount in cents
      status: "pending",
    },
  });

  return session;
}

export async function handleStripeWebhook(/* parameters */) {
  // implement the logic to handle Stripe webhooks
}

export async function handleSuccessfulPayment(sessionId: string) {
  try {
    const transaction = await prisma.stripeTransaction.findUnique({
      where: { stripeSessionId: sessionId },
    });

    if (transaction && transaction.status === 'completed') {
      return transaction; // Payment already processed
    }

    const session = await stripe.checkout.sessions.retrieve(sessionId);
    
    if (session.payment_status !== 'paid') {
      return null; // Payment not successful
    }

    const userId = session.metadata?.userId;
    const credits = parseInt(session.metadata?.credits || "0", 10);

    if (!userId || !credits) {
      throw new Error("Invalid metadata in session");
    }

    // Update transaction status
    const updatedTransaction = await prisma.stripeTransaction.update({
      where: { stripeSessionId: sessionId },
      data: { 
        status: "completed",
        stripePaymentIntentId: session.payment_intent as string || null
      },
    });

    // Update user credits
    await prisma.user.update({
      where: { id: userId },
      data: {
        credits: { increment: credits },
      },
    });

    // Record credit transaction
    await prisma.creditTransaction.create({
      data: {
        userId,
        amount: credits,
        type: "PURCHASE",
      },
    });

    return updatedTransaction;
  } catch (error) {
    console.error("Error handling successful payment:", error);
    throw error;
  }
}

src/app/api/webhooks/stripe/route.ts

import Stripe from 'stripe';
import { headers } from "next/headers";
import { stripe, handleSuccessfulPayment } from "@/lib/stripe";
import { NextResponse } from "next/server";
import { env } from "@/env.mjs";
import { prisma } from "@/lib/db";

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get("Stripe-Signature") as string;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, env.STRIPE_WEBHOOK_SECRET);
  } catch (err: any) {
    return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    
    try {
      // Verify the transaction exists
      const transaction = await prisma.stripeTransaction.findUnique({
        where: { stripeSessionId: session.id },
      });

      if (!transaction) {
        console.error("Transaction not found for session:", session.id);
        return NextResponse.json({ error: "Transaction not found" }, { status: 404 });
      }

      await handleSuccessfulPayment(session.id);
    } catch (error) {
      console.error("Error processing successful payment:", error);
      // Don't expose internal error details to the client
      return NextResponse.json({ error: "Error processing payment" }, { status: 500 });
    }
  }

  return NextResponse.json({ received: true });
}

Hi @justinmky, thanks for sharing the additional context. From the code, I’m not sure if we should get a 405. Can you share the URL you are putting in the Stripe dashboard?

1 Like