Created
November 24, 2024 10:29
-
-
Save simonorzel26/fd0784ddc7c411b215f90419e5889290 to your computer and use it in GitHub Desktop.
Next.js 14+ App Router: Stripe Webhook Handler for Payment Links with TypeScript, Error Handling, and Database Update
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This can be in a server action or anywhere you create a payment link | |
const paymentLink = await stripe.paymentLinks.create({ | |
line_items: [ | |
{ | |
price: "price_123456789", // Replace with your actual Stripe Price ID | |
quantity: 1, | |
}, | |
], | |
// This encodes the user identification so you can securely reconcile the session on your end after payment processing | |
metadata: { | |
profile_id: profile.id, | |
}, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NextRequest, NextResponse } from "next/server"; | |
import { headers } from "next/headers"; | |
import Stripe from "stripe"; | |
// Prisma db instance or other ORM | |
import { db } from "~/server/db"; | |
// Stripe instance (new Stripe()...) | |
import stripe from "~/server/stripe"; | |
async function constructStripeEvent(body: string, signature: string): Promise<Stripe.Event> { | |
if (!process.env.STRIPE_WEBHOOK_SECRET) { | |
throw new Error("STRIPE_WEBHOOK_SECRET is not set"); | |
} | |
return stripe.webhooks.constructEvent( | |
body, | |
signature, | |
process.env.STRIPE_WEBHOOK_SECRET | |
); | |
} | |
async function handleStripeEvent(event: Stripe.Event): Promise<void> { | |
if (event.type === "checkout.session.completed") { | |
const session = event.data.object as Stripe.Checkout.Session; | |
// Reconcile your database with the original user id/data sent with the payment link (session.metadata.profile_id) | |
await updateProfileSubscriptionStatus(session); | |
} | |
} | |
async function updateProfileSubscriptionStatus(session: Stripe.Checkout.Session): Promise<void> { | |
const profileId = session.metadata?.profile_id; | |
if (!profileId) { | |
throw new Error("No profile_id found in the session"); | |
} | |
await db.user.update({ | |
where: { id: profileId }, | |
data: { isSubscribed: true }, | |
}); | |
} | |
function handleWebhookError(error: unknown): NextResponse { | |
const errorMessage = error instanceof Error ? error.message : "Unknown error"; | |
console.error(`Webhook Error: ${errorMessage}`); | |
return NextResponse.json( | |
{ message: `Webhook Error: ${errorMessage}` }, | |
{ status: 400 } | |
); | |
} | |
export async function POST(req: NextRequest): Promise<NextResponse> { | |
const body = await req.text(); | |
const headersList = headers(); | |
const signature = headersList.get("stripe-signature"); | |
if (!signature) { | |
return NextResponse.json( | |
{ message: "Missing stripe-signature header" }, | |
{ status: 400 } | |
); | |
} | |
try { | |
const event = await constructStripeEvent(body, signature); | |
await handleStripeEvent(event); | |
return NextResponse.json({ success: true }); | |
} catch (error) { | |
return handleWebhookError(error); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment