NextJS provides file based routing, even for API routes. API routes are files that are created inside of the pages/api
folder. By default the API route tries to be as helpful as possible, meaning it will parse the request body based upon the incoming Content-Type
in the headers.
So when a POST
comes in with Content-Type
application/json
you can expect that req.body
will be the parsed payload.
One issue is that with Stripe it needs to use the raw payload to validate the request actually originated from Stripe. You should always validate your Stripe webhook requests, if someone found out your webhook URL you could have people sending fake requests.
To disable this default parsing behavior API routes from Next.js have a config
export option. If you export config
you can set false
on api.bodyParser
. This will disable the above behavior and allow us to verify the raw request.
// pages/api/stripe_hook.js export const config = { api: { bodyParser: false, }, }; const handler = async (req, res) => { if (req.method === "POST") { // Code here } else { res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } }; export default handler;
Now to get stripe setup and verifying install the stripe
package, and import it. We need 2 pieces of information that should come from environment variables. The Stripe secret, as well as the signing key for this specific webhook.
These can both be found in the stripe dashboard.
import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2020-08-27", }); const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
Now that we Stripe installed another lets install micro
, a library built by the team from Vercel. It provides small functions that help dealing with requests. In this case we want to convert our req
to a buffer.
Under the hood it does a few which you can check out here https://github.com/vercel/micro/blob/master/packages/micro/lib/index.js#L136
The stripe.webhooks.constructEvent
function takes 3 arguments. The first being a buffer or string. In this case we pass in a buffer. The second is the stripe-signature
that is on the request headers. The third is our webhook secret.
If all goes well the returned value will be the event, and it handles all the body parsing correctly for you. Otherwise if it failed it will throw an error which is why we wrap it in a try/catch
and return an error for the webhook.
import { buffer } from "micro"; import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2020-08-27", }); const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; const handler = async (req, res) => { if (req.method === "POST") { const buf = await buffer(req); const sig = req.headers["stripe-signature"]; let event; try { event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); } catch (err) { res.status(400).send(`Webhook Error: ${err.message}`); return; } res.json({ received: true }); } else { res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } }; export default handler;
Afterwards you can use the event
object to deal with all the stuff you need to do. Check if a charge
succeeded, or if you received an event you weren't expecting log it out. By this part in the code you know you have received an event that actually originated from Stripe.
if (event.type === "charge.succeeded") { const charge = event.data.object; // Handle successful charge } else { console.warn(`Unhandled event type: ${event.type}`); }