← Blog
·14 min read

Setting Up Stripe With AI? 7 Problems Your AI Assistant Can't Solve For You

AI can write your Stripe webhook handler, but it can't navigate the Dashboard, find your signing secret, or fix a wrong endpoint URL. Here are the 7 config problems you'll hit and exactly where to fix them.

Rod

Founder & Developer

AI tools like Cursor, Claude, and Copilot are great at generating Stripe webhook handlers. The code they produce usually works. The problem is everything around the code — the Stripe Dashboard configuration, the environment variables, the endpoint URLs, the API keys. AI can't click buttons in your Stripe Dashboard. It can't check if your env vars are set in Vercel. It can't look at the webhook delivery logs and tell you the response was a 307.

We built Data Hogo's entire payment system with AI assistance. The code was solid from day one. We still broke payments 7 different ways — all configuration problems that no AI could have prevented or diagnosed.

Here's each problem, the exact error you'll see, and where to fix it.


Problem #1: Wrong Webhook Endpoint URL

This is the most common Stripe webhook failure, and the one AI tools will never catch because it's a Dashboard configuration issue.

What happens: You register a webhook endpoint in Stripe Dashboard. But the URL doesn't exactly match what your server serves. Common mismatches:

  • https://datahogo.com/... vs https://www.datahogo.com/... (www redirect)
  • https://myapp.com/api/webhook vs https://myapp.com/api/webhook/ (trailing slash)
  • https://myapp.com/api/webhooks/stripe vs https://myapp.vercel.app/api/webhooks/stripe (wrong domain)

What you'll see in Stripe Dashboard:

Go to Developers > Webhooks > select your endpoint > Recent deliveries. Click on any failed delivery. The response will look like:

{
  "redirect": "https://www.datahogo.com/api/webhooks/stripe",
  "status": "307"
}

HTTP status code: 307 (or 301, 302). Stripe does not follow redirects. Any non-2xx response = failed delivery.

How to fix it:

  1. Open your production app in a browser and look at the URL bar — does it have www? Does it have a trailing slash?
  2. Go to Stripe Dashboard > Developers > Webhooks
  3. Click your endpoint, then Update details
  4. Change the URL to exactly match your production URL

Your AI assistant wrote a perfectly good handler. It just never received a single request because the URL was wrong.


Problem #2: Where to Find Your API Keys (The Dashboard Treasure Hunt)

AI will generate code with process.env.STRIPE_SECRET_KEY and process.env.STRIPE_WEBHOOK_SECRET, but it can't tell you where to get those values. Here's the exact navigation:

STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY

Stripe Dashboard > Developers > API keys

Key Starts with Use in
Publishable key pk_live_ or pk_test_ Frontend (safe to expose)
Secret key sk_live_ or sk_test_ Backend only (never expose)

Click "Reveal live key" or "Reveal test key" to copy. If you've never revealed it, you may need to re-create it.

STRIPE_WEBHOOK_SECRET

Stripe Dashboard > Developers > Webhooks > click your endpoint > Signing secret > Reveal

Starts with whsec_.

There are THREE different webhook secrets:

Environment Where to get it Notes
Local dev Run stripe listen --forward-to localhost:3000/api/webhooks/stripe — the CLI prints a whsec_ secret Changes every time you restart the CLI
Test mode Stripe Dashboard (toggle to Test mode) > Webhooks > endpoint > Signing secret Persistent
Production Stripe Dashboard (Live mode) > Webhooks > endpoint > Signing secret Persistent

Mixing these up always results in a 401 Invalid signature with no other useful information. If your webhook works locally but fails in production, this is almost certainly why.


Problem #3: Environment Variables Not in Production

Your AI generated the code. You tested locally. It works. You deploy to Vercel. Every webhook returns 401.

Why: You added STRIPE_WEBHOOK_SECRET to .env.local but never added it to your hosting provider.

What you'll see in Stripe Dashboard: Every delivery shows HTTP status 401 with response "Invalid signature".

How to fix it in Vercel:

  1. Vercel Dashboard > your project > Settings > Environment Variables
  2. Add STRIPE_WEBHOOK_SECRET with the value from Stripe Dashboard (live mode)
  3. Make sure "Production" is checked (not just Preview/Development)
  4. Redeploy — env vars don't apply to existing deployments

Full list of Stripe env vars you need in production:

Variable Required Where to get it
STRIPE_SECRET_KEY Yes Developers > API keys > Secret key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY Yes Developers > API keys > Publishable key
STRIPE_WEBHOOK_SECRET Yes Developers > Webhooks > endpoint > Signing secret

If you're not sure which env vars you've accidentally left out, this happens more often than you'd think. Our env file exposure guide covers why env handling is a constant source of production incidents.


Problem #4: API Version Mismatch — RangeError: Invalid time value

This is the sneakiest one. Everything works fine for weeks, then one day your webhook crashes with a RangeError and you have no idea why.

What happens: Stripe evolves its API. Your code (probably AI-generated) was written for one API version, but Stripe sends webhook events using the version configured on your Dashboard account. When fields move between objects in a new version, your code reads undefined where it expected data.

Real example we hit:

Our code used API version 2024-12-18.acacia:

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-12-18.acacia",
});

Stripe's Dashboard was on 2025-10-29.clover. In this version, current_period_start and current_period_end moved from the Subscription object to SubscriptionItem (inside items.data[0]).

Our handler did:

// This worked for months, then suddenly broke
const periodStart = new Date(subscription.current_period_start * 1000).toISOString();

After the API version change:

  • subscription.current_period_start is undefined
  • undefined * 1000 = NaN
  • new Date(NaN).toISOString() throws RangeError: Invalid time value

The fix: Read from both locations:

const firstItem = subscription.items?.data?.[0] as any;
const periodStart =
  (subscription as any).current_period_start ?? firstItem?.current_period_start;
const periodEnd =
  (subscription as any).current_period_end ?? firstItem?.current_period_end;
 
if (periodStart) {
  const startDate = new Date(periodStart * 1000).toISOString();
  // use startDate...
}

How to check your API version:

  • Your code: look at the apiVersion in your Stripe client initialization
  • Your Dashboard: Stripe Dashboard > Developers > Overview shows "API version" at the top
  • Webhook events: every event payload includes "api_version" — check a recent delivery

If they don't match, you'll eventually hit breaking changes. AI won't catch this because it doesn't know what API version your Dashboard is on.


Problem #5: Not Logging Webhook Errors

AI-generated webhook handlers usually have basic error handling, but they rarely log enough detail to debug in production.

What AI typically generates:

try {
  event = stripe.webhooks.constructEvent(body, sig!, secret);
} catch {
  return new Response("Invalid signature", { status: 401 });
}

This catches the error but tells you nothing. Was the secret wrong? Was the body already parsed? Was the header missing?

What you actually need:

try {
  event = stripe.webhooks.constructEvent(body, sig!, secret);
} catch (err) {
  console.error("[stripe-webhook] Signature verification failed:",
    err instanceof Error ? err.message : err
  );
  return new Response("Invalid signature", { status: 401 });
}

Common error messages from Stripe and what they mean:

Error message Cause
No signatures found matching the expected signature for payload Wrong STRIPE_WEBHOOK_SECRET or body was parsed with req.json() before verification
Timestamp outside the tolerance zone Clock sync issue or event is very old
Webhook payload must be provided as a string You passed parsed JSON instead of raw text

Same goes for the main handler — wrap everything in a try/catch and log with context:

try {
  await handleStripeEvent(event);
} catch (err) {
  console.error("[stripe-webhook] Processing failed:", {
    eventId: event.id,
    eventType: event.type,
    error: err instanceof Error ? err.message : err,
  });
  // Add Sentry if you have it
  Sentry.captureException(err, {
    extra: { eventType: event.type, eventId: event.id },
  });
  return new Response("Processing error", { status: 500 });
}

If your security posture could use a second opinion, scan your repo with Data Hogo — we flag missing error handling patterns in webhook routes among other things.


Problem #6: Reading Stripe's Webhook Logs

When something goes wrong, the answer is almost always in the Stripe Dashboard logs. But most developers don't know where to look or what the status codes mean.

Where to check:

Stripe Dashboard > Developers > Webhooks > select your endpoint > Recent deliveries

Each delivery shows:

  • HTTP status code — the response from your server
  • Response body — what your server returned
  • Request body — the full event payload Stripe sent

What the status codes mean:

Status What happened Where the problem is
200 Success Nothing — it worked
307 (or 301, 302) Your URL redirected Dashboard: wrong endpoint URL. Fix: update the webhook URL
401 Signature verification failed Missing or wrong STRIPE_WEBHOOK_SECRET in your env
404 Route not found Wrong path in the endpoint URL, or the route doesn't exist in your deployment
500 Your handler crashed Unhandled exception in your code — check your server logs
Timeout Handler took too long Your handler is doing too much work synchronously

Where to find individual events:

Stripe Dashboard > Developers > Events

Here you can filter by event type, see all delivery attempts, and Resend events that failed. After you fix a bug, come here and resend the failed events one by one.

AI can write your handler code, but it can't read these logs for you. When something breaks, this is where you start.


Problem #7: Stripe's Retry Behavior (What Happens After Failure)

When a webhook delivery fails, Stripe doesn't just give up. It retries with exponential backoff over 3 days:

  • Immediately
  • ~5 minutes later
  • ~30 minutes
  • ~2 hours
  • ~5 hours
  • ~10 hours
  • (continues spacing out)

What this means for you:

  1. Don't panic. You have 3 days to fix a broken webhook before Stripe stops trying.
  2. Don't deploy a half-fix. Take the time to find the actual root cause.
  3. After fixing, resend failed events from Developers > Events > select event > Resend.
  4. If Stripe disabled your endpoint (after 3 days of failures), re-enable it from Developers > Webhooks > select endpoint.

Stripe also sends you an email when a webhook endpoint starts failing consistently. Check your inbox — we got ours with the subject "Action required: webhook endpoint is failing" with the exact failure count and date.


Complete Webhook Handler Template

Here's a production-ready Next.js App Router webhook handler that handles all 7 problems:

// app/api/webhooks/stripe/route.ts
import type Stripe from "stripe";
import * as Sentry from "@sentry/nextjs";
import { constructWebhookEvent } from "@/lib/stripe/client";
 
export async function POST(req: Request) {
  // 1. Read raw body — NEVER use req.json() (breaks signature verification)
  const body = await req.text();
  const signature = req.headers.get("stripe-signature");
 
  if (!signature) {
    return Response.json({ error: "Missing signature" }, { status: 401 });
  }
 
  // 2. Verify signature — LOG the error, don't swallow it
  let event: Stripe.Event;
  try {
    event = constructWebhookEvent(body, signature);
  } catch (err) {
    console.error("[stripe-webhook] Signature verification failed:",
      err instanceof Error ? err.message : err
    );
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }
 
  // 3. Global try/catch — never let an unhandled exception return 500
  try {
    await handleStripeEvent(event);
  } catch (err) {
    console.error("[stripe-webhook] Unhandled error:", {
      eventType: event.type,
      eventId: event.id,
      error: err instanceof Error ? err.message : err,
    });
    Sentry.captureException(err, {
      extra: { eventType: event.type, eventId: event.id },
    });
    return Response.json({ received: true, error: "Processing error" });
  }
 
  return Response.json({ received: true });
}
 
async function handleStripeEvent(event: Stripe.Event): Promise<void> {
  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      const userId = session.metadata?.userId;
      const plan = session.metadata?.plan;
      if (!userId || !plan) break;
      // activate subscription...
      break;
    }
 
    case "customer.subscription.updated": {
      const subscription = event.data.object as Stripe.Subscription;
 
      // 4. API version safe — handles field migration between versions
      const firstItem = subscription.items?.data?.[0] as any;
      const periodStart =
        (subscription as any).current_period_start ?? firstItem?.current_period_start;
      const periodEnd =
        (subscription as any).current_period_end ?? firstItem?.current_period_end;
 
      if (periodStart && periodEnd) {
        const startDate = new Date(periodStart * 1000).toISOString();
        const endDate = new Date(periodEnd * 1000).toISOString();
        // update subscription periods...
      }
      break;
    }
 
    case "customer.subscription.deleted": {
      // downgrade user...
      break;
    }
 
    case "invoice.payment_failed": {
      // mark subscription as past_due...
      break;
    }
  }
}

Pre-deployment checklist:

  • Webhook URL in Stripe Dashboard matches your exact production URL (including www)
  • STRIPE_SECRET_KEY is in your hosting provider's env vars (not just .env.local)
  • STRIPE_WEBHOOK_SECRET is in your hosting provider's env vars (live mode secret, not CLI secret)
  • You've tested a webhook delivery in the Stripe Dashboard and seen a 200 response
  • Error monitoring (Sentry or similar) is wired up to catch webhook errors

Frequently Asked Questions

Why is my Stripe webhook returning 307?

Your endpoint URL in the Stripe Dashboard doesn't match your production URL. Stripe does not follow redirects. Go to Developers > Webhooks, click on a failed delivery, and look at the response — it will show the redirect URL. Update your endpoint URL to match exactly.

Where do I find my Stripe webhook signing secret?

Stripe Dashboard > Developers > Webhooks > select your endpoint > Signing secret > Reveal. It starts with whsec_. Remember: local CLI, test mode, and live mode all have different secrets.

Why does my Stripe webhook work locally but fail in production?

Almost always missing env vars. STRIPE_WEBHOOK_SECRET is in your .env.local but not in Vercel/Railway/whatever you deploy to. Also check that you're using the production signing secret (from the Dashboard), not the CLI secret from stripe listen.

What causes RangeError: Invalid time value in a Stripe webhook?

API version mismatch. Fields like current_period_start moved between objects in newer Stripe API versions. Your code reads undefined, multiplies by 1000 to get NaN, and new Date(NaN).toISOString() throws. Read from both the old and new locations with a ?? fallback.

How do I resend a failed Stripe webhook event?

Stripe Dashboard > Developers > Events > find the event > Resend. You can resend individual events after fixing your endpoint. Stripe retries automatically for 3 days before giving up.

What Stripe API keys do I need and where are they?

Three keys: (1) STRIPE_SECRET_KEY — Developers > API keys > Secret key (starts with sk_live_). (2) NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY — same page (starts with pk_live_). (3) STRIPE_WEBHOOK_SECRET — Developers > Webhooks > endpoint > Signing secret (starts with whsec_). Never expose the secret key in frontend code.

stripewebhooksnext.jspaymentsdebuggingapi integrationai coding