Last active
August 6, 2024 20:08
-
-
Save amosbastian/e403e1d8ccf4f7153f7840dd11a85a69 to your computer and use it in GitHub Desktop.
Lemon Squeezy webhook using the new route handler in Next.js 13
This file contains 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 crypto from "crypto"; | |
import { listAllSubscriptions } from "lemonsqueezy.ts"; | |
import { NextRequest } from "next/server"; | |
// Put this in your billing lib and just import the type instead | |
type LemonsqueezySubscription = Awaited<ReturnType<typeof listAllSubscriptions>>["data"][number]; | |
const isError = (error: unknown): error is Error => { | |
return error instanceof Error; | |
}; | |
// Add more events here if you want | |
// https://docs.lemonsqueezy.com/api/webhooks#event-types | |
type EventName = | |
| "order_created" | |
| "order_refunded" | |
| "subscription_created" | |
| "subscription_cancelled" | |
| "subscription_resumed" | |
| "subscription_expired" | |
| "subscription_paused" | |
| "subscription_unpaused" | |
| "subscription_payment_failed" | |
| "subscription_payment_success" | |
| "subscription_payment_recovered"; | |
type Payload = { | |
meta: { | |
test_mode: boolean; | |
event_name: EventName; | |
}; | |
// Possibly not accurate: it's missing the relationships field and any custom data you add | |
data: LemonsqueezySubscription; | |
}; | |
export const POST = async (request: NextRequest) => { | |
try { | |
const text = await request.text(); | |
const hmac = crypto.createHmac("sha256", process.env["LEMON_SQUEEZY_WEBHOOK_SECRET"] || ""); | |
const digest = Buffer.from(hmac.update(text).digest("hex"), "utf8"); | |
const signature = Buffer.from(request.headers.get("x-signature") as string, "utf8"); | |
if (!crypto.timingSafeEqual(digest, signature)) { | |
return new Response("Invalid signature.", { | |
status: 400, | |
}); | |
} | |
const payload = JSON.parse(text); | |
const { | |
meta: { event_name: eventName }, | |
data: subscription, | |
} = payload as Payload; | |
switch (eventName) { | |
case "order_created": | |
// Do stuff here if you are using orders | |
break; | |
case "order_refunded": | |
// Do stuff here if you are using orders | |
break; | |
case "subscription_created": | |
case "subscription_cancelled": | |
case "subscription_resumed": | |
case "subscription_expired": | |
case "subscription_paused": | |
case "subscription_unpaused": | |
case "subscription_payment_failed": | |
case "subscription_payment_success": | |
case "subscription_payment_recovered": | |
// Do something with the subscription here, like syncing to your database | |
console.log(subscription); | |
break; | |
default: | |
throw new Error(`🤷♀️ Unhandled event: ${eventName}`); | |
} | |
} catch (error: unknown) { | |
if (isError(error)) { | |
return new Response(`Webhook error: ${error.message}`, { | |
status: 400, | |
}); | |
} | |
return new Response("Webhook error", { | |
status: 400, | |
}); | |
} | |
return new Response(null, { | |
status: 200, | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks a lot, long live open source!