@creem_io/nextjs
v0.6.0
Published
Creem payment integration for Next.js applications
Readme
Installation · Quick Start · Documentation
Introduction
@creem_io/nextjs is the official Next.js integration for Creem - a modern payment platform. This library provides:
- 🎨 React Components - Drop-in components for checkout and customer portal
- 🔐 Type-Safe - Full TypeScript support with comprehensive type definitions
- ⚡ Zero Config - Works out of the box with Next.js App Router
- 🪝 Webhook Management - Simple, type-safe webhook handlers with automatic verification
- 🔄 Subscription Lifecycle - Built-in access management for subscription-based products
Installation
Install the package using your preferred package manager:
npm
npm install @creem_io/nextjsyarn
yarn add @creem_io/nextjspnpm
pnpm install @creem_io/nextjsRequirements
- Next.js 13.0.0 or higher (App Router)
- React 18.0.0 or higher
- A Creem account with API keys
Note: This package uses the Creem SDK v1.3.6+ internally. The SDK is bundled as a dependency.
Quick Start
1. Set up your environment variables
Create a .env.local file in your project root:
CREEM_API_KEY=your_api_key_here
CREEM_WEBHOOK_SECRET=your_webhook_secret_here2. Create a checkout route
Create app/checkout/route.ts:
import { Checkout } from "@creem_io/nextjs";
export const GET = Checkout({
apiKey: process.env.CREEM_API_KEY!,
testMode: true, // Set to false for production
});3. Add a checkout button to your page
In your app/page.tsx or any client component:
"use client";
import { CreemCheckout } from "@creem_io/nextjs";
export default function Page() {
return (
<CreemCheckout productId="prod_abc123" successUrl="/thank-you">
<button className="btn-primary">Subscribe Now</button>
</CreemCheckout>
);
}4. Handle webhooks
Create app/api/webhook/creem/route.ts:
import { Webhook } from "@creem_io/nextjs";
export const POST = Webhook({
webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
onCheckoutCompleted: async ({ customer, product }) => {
console.log(`${customer.email} purchased ${product.name}`);
},
onGrantAccess: async ({ reason, customer, metadata }) => {
const userId = metadata?.referenceId as string;
// Grant user access to your platform
},
onRevokeAccess: async ({ reason, customer, metadata }) => {
const userId = metadata?.referenceId as string;
// Revoke user access from your platform
},
});That's it! You now have a working payment integration in Next.js. 🎉
Documentation
Client Components
<CreemCheckout />
A React component that creates a checkout link. When clicked, redirects users to your checkout route handler which creates a Creem checkout session.
import { CreemCheckout } from "@creem_io/nextjs";
<CreemCheckout
productId="prod_abc123"
units={2}
discountCode="SUMMER2024"
customer={{
email: "[email protected]",
name: "John Doe",
}}
successUrl="/thank-you"
metadata={{
orderId: "12345",
source: "web",
}}
referenceId="user_123"
>
<button>Buy Now</button>
</CreemCheckout>;Props:
| Prop | Type | Required | Description |
| -------------- | ----------- | -------- | ----------------------------------------------- |
| productId | string | ✅ | The Creem product ID from your dashboard |
| units | number | ❌ | Number of units to purchase (default: 1) |
| discountCode | string | ❌ | Discount code to apply |
| customer | object | ❌ | Pre-fill customer information (email, name) |
| successUrl | string | ❌ | URL to redirect after successful payment |
| metadata | object | ❌ | Custom metadata to attach to the checkout |
| referenceId | string | ❌ | Your internal user/order ID |
| customFields | object | ❌ | Custom fields to store with the checkout |
| checkoutPath | string | ❌ | Custom API route path (default: /checkout) |
| children | ReactNode | ❌ | Custom button or link content |
<CreemPortal />
A React component that creates a customer portal link for managing subscriptions, payment methods, and billing history.
import { CreemPortal } from "@creem_io/nextjs";
<CreemPortal
customerId="cust_abc123"
className="btn-secondary"
>
Manage Subscription
</CreemPortal>;Props:
| Prop | Type | Required | Description |
| ------------ | ------------------- | -------- | ---------------------------------------------------------- |
| customerId | string | ✅ | The Creem customer ID |
| portalPath | string | ❌ | Custom API route path (default: /portal) |
| children | ReactNode | ❌ | Custom button or link content |
| ...linkProps | HTMLAnchorElement | ❌ | Any standard anchor tag props (className, style, etc.) |
Server Functions
Checkout(options)
Creates a Next.js route handler for checkout sessions.
import { Checkout } from "@creem_io/nextjs";
// app/checkout/route.ts
export const GET = Checkout({
apiKey: process.env.CREEM_API_KEY!,
testMode: true,
defaultSuccessUrl: "/success",
});Options:
| Option | Type | Required | Description |
| ------------------- | --------- | -------- | ------------------------------------------------ |
| apiKey | string | ✅ | Your Creem API key |
| testMode | boolean | ❌ | Use test environment (default: false) |
| defaultSuccessUrl | string | ❌ | Default success URL if not provided in component |
Portal(options)
Creates a Next.js route handler for customer portal sessions.
import { Portal } from "@creem_io/nextjs";
// app/portal/route.ts
export const GET = Portal({
apiKey: process.env.CREEM_API_KEY!,
testMode: true,
});Options:
| Option | Type | Required | Description |
| ---------- | --------- | -------- | --------------------------------------- |
| apiKey | string | ✅ | Your Creem API key |
| testMode | boolean | ❌ | Use test environment (default: false) |
Webhook(options)
Creates a Next.js route handler for processing Creem webhooks with automatic signature verification.
import { Webhook } from "@creem_io/nextjs";
// app/api/webhook/creem/route.ts
export const POST = Webhook({
webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
onCheckoutCompleted: async (data) => {
// Handle one-time purchases
},
onGrantAccess: async (context) => {
// Grant user access (subscription active/trialing/paid)
},
onRevokeAccess: async (context) => {
// Revoke user access (subscription paused/expired)
},
});Options:
| Option | Type | Required | Description |
| ------------------------ | ---------- | -------- | ---------------------------------------------------- |
| webhookSecret | string | ✅ | Your Creem webhook secret for signature verification |
| onCheckoutCompleted | function | ❌ | Called when checkout is completed |
| onRefundCreated | function | ❌ | Called when refund is created |
| onDisputeCreated | function | ❌ | Called when dispute is created |
| onSubscriptionActive | function | ❌ | Called when subscription becomes active |
| onSubscriptionTrialing | function | ❌ | Called when subscription is trialing |
| onSubscriptionPaid | function | ❌ | Called when subscription payment succeeds |
| onSubscriptionExpired | function | ❌ | Called when subscription expires |
| onSubscriptionCanceled | function | ❌ | Called when subscription is canceled |
| onSubscriptionUnpaid | function | ❌ | Called when subscription payment fails |
| onSubscriptionPastDue | function | ❌ | Called when subscription is past due |
| onSubscriptionPaused | function | ❌ | Called when subscription is paused |
| onSubscriptionUpdate | function | ❌ | Called when subscription is updated |
| onGrantAccess | function | ❌ | Called when user should be granted access |
| onRevokeAccess | function | ❌ | Called when user access should be revoked |
Access Management
The onGrantAccess and onRevokeAccess callbacks provide a simple way to manage user access for subscription-based products.
onGrantAccess
Called when a user should be granted access. This happens when:
- Subscription becomes active (after payment)
- Subscription enters trialing period (free trial)
- Subscription payment is paid (renewal)
onGrantAccess: async ({ reason, customer, product, metadata }) => {
const userId = metadata?.referenceId as string;
// Grant access in your database
await db.user.update({
where: { id: userId },
data: { subscriptionActive: true },
});
console.log(`Granted ${reason} to ${customer.email}`);
};onRevokeAccess
Called when a user's access should be revoked. This happens when:
- Subscription is paused (manually by user or admin)
- Subscription is expired (trial ended or canceled subscription period ended)
onRevokeAccess: async ({ reason, customer, product, metadata }) => {
const userId = metadata?.referenceId as string;
// Revoke access in your database
await db.user.update({
where: { id: userId },
data: { subscriptionActive: false },
});
console.log(`Revoked access (${reason}) from ${customer.email}`);
};⚠️ Important: Both callbacks may be called multiple times for the same user/subscription. Always implement these as idempotent operations (safe to call repeatedly).
Discount Codes
Apply discount codes to checkouts:
<CreemCheckout
productId="prod_abc123"
discountCode="LAUNCH50"
successUrl="/thank-you"
>
<button>Subscribe with 50% off</button>
</CreemCheckout>Custom Metadata & Reference IDs
Track your internal IDs and custom data:
<CreemCheckout
productId="prod_abc123"
referenceId={user.id} // Your user ID
metadata={{
orderId: generateOrderId(),
source: "mobile_app",
campaign: "summer_sale",
affiliateId: "partner_123",
}}
successUrl="/thank-you"
>
<button>Subscribe Now</button>
</CreemCheckout>Access in webhooks:
onCheckoutCompleted: async ({ metadata }) => {
const { orderId, source, campaign, affiliateId } = metadata;
// Use your custom data
};Best Practices
1. Always Use Environment Variables
Never hardcode API keys or webhook secrets:
// ✅ Good
apiKey: process.env.CREEM_API_KEY!;
// ❌ Bad
apiKey: "sk_live_abc123...";2. Implement Idempotent Access Management
Access callbacks may be called multiple times. Always make them idempotent:
// ✅ Good - idempotent
onGrantAccess: async ({ customer, metadata }) => {
await db.user.upsert({
where: { id: metadata.referenceId },
update: { subscriptionActive: true },
create: { id: metadata.referenceId, subscriptionActive: true },
});
};
// ❌ Bad - not idempotent
onGrantAccess: async ({ customer, metadata }) => {
await db.user.create({
/* will fail on duplicate calls */
});
};3. Use Reference IDs
Always pass your internal user ID as referenceId:
<CreemCheckout
productId="prod_abc123"
referenceId={session.user.id}
// ...
/>This allows you to easily map Creem customers to your users in webhooks.
4. Test in Test Mode
Always test your integration in test mode first:
export const GET = Checkout({
apiKey: process.env.CREEM_API_KEY!,
testMode: true, // Test mode
});5. Handle Errors Gracefully
Webhook handlers should handle errors and return appropriate responses:
onGrantAccess: async (context) => {
try {
await grantUserAccess(context);
} catch (error) {
console.error("Failed to grant access:", error);
// Don't throw - webhook will retry
}
};Webhook Configuration
Setting Up Webhooks in Creem Dashboard
- Go to your Creem Dashboard
- Navigate to Developers → Webhooks
- Click Add Endpoint
- Enter your webhook URL:
https://yourdomain.com/api/webhook/creem - Select the events you want to receive
- Copy the Webhook Secret to your
.env.localfile
Testing Webhooks Locally
Use a tool like ngrok to expose your local server:
ngrok http 3000Then use the ngrok URL in your Creem webhook settings:
https://abc123.ngrok.io/api/webhook/creemContributing
We welcome contributions! Please reach out on Discord for details.
Development Setup
# Clone the repository
git clone https://github.com/armitage-labs/creem-nextjs-adaptor.git
# Install dependencies
npm install
# Build the library
npm run build
# Run example app
cd example
npm install
npm run devSupport
- 📧 Email: [email protected]
- 💬 Discord: Join our community
- 📚 Documentation: docs.creem.io
Authors
Built with ❤️ by the Creem team.
