中文 | Español | Français | Deutsch | Nederlands | Polski
Integrates Creem payments, subscriptions, and billing into your Convex application.
Creem is a Merchant of Record that handles global payments, tax compliance, and subscription management. This Convex component brings Creem's full payment stack into your Convex backend with real-time reactivity.
Check out the example app for a complete demo.
// Get subscription details for the current user
const subscription = await creem.getCurrentSubscription(ctx, { userId });
// Show available plans
<CheckoutLink creemApi={api.creem} productId="prod_xxx">
Upgrade to Pro
</CheckoutLink>
// Manage existing subscriptions
<CustomerPortalLink creemApi={api.creem}>
Manage Subscription
</CustomerPortalLink>You'll need a Convex app to use this component. Follow any of the Convex quickstarts to set one up.
- Create a Creem account
- Create products in the Creem dashboard
- Get your API key from Settings > API Keys
- Get your webhook secret from Developers > Webhooks
Install the component package:
npm install @creem_io/convexCreate a convex.config.ts file in your app's convex/ folder and install the component by calling app.use:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import creem from "@creem_io/convex/convex.config.js";
const app = defineApp();
app.use(creem);
export default app;Set your Creem credentials as environment variables:
npx convex env set CREEM_API_KEY creem_xxxxx
npx convex env set CREEM_WEBHOOK_SECRET whsec_xxxxx
# Optional: set to "production" for live mode (defaults to "test")
npx convex env set CREEM_ENVIRONMENT test// convex/creem.ts
import { Creem } from "@creem_io/convex";
import { api, components } from "./_generated/api";
export const creem = new Creem(components.creem, {
// Required: provide a function to get the current user's ID and email
getUserInfo: async (ctx) => {
const user = await ctx.runQuery(api.users.getCurrentUser);
return { userId: user._id, email: user.email };
},
// Optional: map friendly keys to Creem product IDs
products: {
proMonthly: "prod_xxxxx",
proYearly: "prod_yyyyy",
},
});
// Export API functions
export const {
generateCheckoutLink,
generateCustomerPortalUrl,
cancelCurrentSubscription,
upgradeCurrentSubscription,
getConfiguredProducts,
listAllProducts,
syncProducts,
} = creem.api();Register the webhook handler in your convex/http.ts:
// convex/http.ts
import { httpRouter } from "convex/server";
import { creem } from "./creem";
const http = httpRouter();
creem.registerRoutes(http as any, {
// Optional: custom path (defaults to "/creem/webhook")
path: "/creem/webhook",
// Optional: callbacks for webhook events
onCheckoutCompleted: async (ctx, event) => {
console.log("Checkout completed:", event.object);
},
onSubscriptionCanceled: async (ctx, event) => {
console.log("Subscription canceled:", event.object);
},
});
export default http;Then register the webhook URL in your Creem Dashboard:
https://<your-convex-site-url>/creem/webhook
Your Convex site URL can be found in your Convex dashboard or via the CONVEX_SITE_URL system environment variable.
If you created products before installing this component, sync them:
// Run once from your Convex dashboard or via a one-off action
await creem.syncProducts(ctx);Or export the action and call it:
// The syncProducts function is already exported from creem.api()
// Call it from the Convex dashboard or your appimport { CheckoutLink, CustomerPortalLink } from "@creem_io/convex/react";
import { api } from "../convex/_generated/api";
function PricingPage() {
const products = useQuery(api.creem.getConfiguredProducts);
return (
<div>
{products?.proMonthly && (
<CheckoutLink
creemApi={api.creem}
productId={products.proMonthly.creemProductId}
>
Subscribe Monthly - ${(products.proMonthly.price / 100).toFixed(2)}/mo
</CheckoutLink>
)}
<CustomerPortalLink creemApi={api.creem}>
Manage Subscription
</CustomerPortalLink>
</div>
);
}Query subscription information with real-time updates:
// convex/users.ts
import { query } from "./_generated/server";
import { creem } from "./creem";
export const getCurrentUserWithSubscription = query({
handler: async (ctx) => {
const user = await getAuthenticatedUser(ctx);
const subscription = await creem.getCurrentSubscription(ctx, {
userId: user._id,
});
return {
...user,
subscription,
isPro: subscription?.status === "active",
productKey: subscription?.productKey, // e.g. "proMonthly"
};
},
});// Cancel subscription
const cancel = useAction(api.creem.cancelCurrentSubscription);
await cancel({ mode: "immediate" }); // or "scheduled"
// Upgrade subscription
const upgrade = useAction(api.creem.upgradeCurrentSubscription);
await upgrade({ productId: "prod_new_plan_id" });Use the high-level onGrantAccess / onRevokeAccess callbacks for the most common use case — granting and revoking access:
creem.registerRoutes(http as any, {
onGrantAccess: async (ctx, { userId, productId, subscriptionId }) => {
// Fires on: checkout.completed, subscription.active, subscription.paid
if (userId) {
await ctx.runMutation(api.users.grantPremiumAccess, { userId, productId });
}
},
onRevokeAccess: async (ctx, { userId, subscriptionId, reason }) => {
// Fires on: subscription.canceled, subscription.expired
if (userId) {
await ctx.runMutation(api.users.revokePremiumAccess, { userId });
}
},
});For more control, use specific per-event callbacks. These can be combined with onGrantAccess/onRevokeAccess:
creem.registerRoutes(http as any, {
onCheckoutCompleted: async (ctx, event) => {
// Grant access after purchase
const userId = event.object.metadata?.userId;
if (userId) {
await ctx.runMutation(api.users.grantPremiumAccess, { userId });
}
},
onSubscriptionCanceled: async (ctx, event) => {
// Revoke access on cancellation
},
onSubscriptionPaid: async (ctx, event) => {
// Handle renewal payments
},
onRefundCreated: async (ctx, event) => {
// Handle refunds
},
});Validate, activate, and deactivate license keys for software distribution:
const { validateLicense, activateLicense, deactivateLicense } = creem.api();
// Validate a license key
const result = await validateLicense({ key: "LIC-KEY-123" });
// Activate on a device
const activated = await activateLicense({
key: "LIC-KEY-123",
instanceName: "User's MacBook",
});
// Deactivate a device
await deactivateLicense({
key: "LIC-KEY-123",
instanceId: "inst_abc",
});Create and manage promotional discounts:
const { createDiscount, getDiscount, deleteDiscount } = creem.api();
// Create a 20% off discount
await createDiscount({
code: "SAVE20",
type: "percentage",
amount: 20,
maxRedemptions: 100,
});
// Apply at checkout
<CheckoutLink
creemApi={api.creem}
productId="prod_xxx"
discountCode="SAVE20"
>
Get 20% Off
</CheckoutLink>Query payment transactions directly:
const { getTransaction, listTransactions } = creem.api();
// Get a specific transaction
const tx = await getTransaction({ transactionId: "tx_xxx" });
// List all transactions
const txList = await listTransactions({ pageNumber: 1, pageSize: 20 });The Creem class accepts a configuration object:
| Option | Type | Description |
|---|---|---|
getUserInfo |
(ctx) => Promise<{userId, email}> |
Required. Returns current user info. |
products |
Record<string, string> |
Map of keys to Creem product IDs. |
apiKey |
string |
Creem API key. Defaults to CREEM_API_KEY env var. |
webhookSecret |
string |
Webhook secret. Defaults to CREEM_WEBHOOK_SECRET env var. |
environment |
"production" | "test" |
API environment. Defaults to CREEM_ENVIRONMENT env var or "test". |
| Method | Description |
|---|---|
getCurrentSubscription(ctx, { userId }) |
Get active subscription for a user |
listUserSubscriptions(ctx, { userId }) |
List all subscriptions for a user |
listProducts(ctx, { activeOnly? }) |
List all synced products |
getProduct(ctx, { productId }) |
Get a specific product |
listUserOrders(ctx, { userId }) |
List all orders for a user |
getCustomerByUserId(ctx, { userId }) |
Get customer record |
| Method | Description |
|---|---|
createCheckoutSession(ctx, args) |
Create a checkout and return the URL |
cancelSubscription(ctx, opts?) |
Cancel the current user's subscription |
upgradeSubscription(ctx, { productId }) |
Upgrade to a different product |
pauseSubscription(ctx) |
Pause the current user's subscription |
resumeSubscription(ctx) |
Resume a paused subscription |
generateCustomerPortalUrl(ctx) |
Get the customer portal URL |
syncProducts(ctx) |
Sync all products from Creem |
| Function | Type | Description |
|---|---|---|
generateCheckoutLink |
Action | Create checkout link for a product |
generateCustomerPortalUrl |
Action | Get customer portal URL |
cancelCurrentSubscription |
Action | Cancel current subscription |
upgradeCurrentSubscription |
Action | Upgrade subscription |
updateCurrentSubscription |
Action | Update subscription (units, metadata) |
pauseCurrentSubscription |
Action | Pause current subscription |
resumeCurrentSubscription |
Action | Resume paused subscription |
createProduct |
Action | Create a new product via Creem API |
getConfiguredProducts |
Query | Get products by configured keys |
listAllProducts |
Query | List all synced products |
getCurrentSubscription |
Query | Get active subscription for current user |
listUserSubscriptions |
Query | List all subscriptions for current user |
listUserOrders |
Query | List all orders for current user |
syncProducts |
Action | Sync products from Creem API |
validateLicense |
Action | Validate a license key |
activateLicense |
Action | Activate a license on a device |
deactivateLicense |
Action | Deactivate a license instance |
createDiscount |
Action | Create a promotional discount code |
getDiscount |
Action | Retrieve discount details |
deleteDiscount |
Action | Delete a discount code |
getTransaction |
Action | Get a transaction by ID |
listTransactions |
Action | List payment transactions |
getCustomerFromCreem |
Action | Retrieve customer from Creem API |
listCustomersFromCreem |
Action | List all customers from Creem API |
| Prop | Type | Description |
|---|---|---|
creemApi |
object | Object with generateCheckoutLink function |
productId |
string |
Creem product ID |
successUrl |
string? |
Redirect URL after checkout |
metadata |
object? |
Custom metadata for the checkout |
discountCode |
string? |
Pre-fill discount code |
className |
string? |
CSS class |
Same props as CheckoutLink, but eagerly fetches the checkout URL on mount for instant click-through.
| Prop | Type | Description |
|---|---|---|
creemApi |
object | Object with generateCustomerPortalUrl function |
className |
string? |
CSS class |
All events are automatically synced. Supported event types:
| Event | Description |
|---|---|
checkout.completed |
Checkout session completed |
subscription.active |
New subscription created |
subscription.paid |
Subscription payment collected |
subscription.canceled |
Subscription canceled |
subscription.scheduled_cancel |
Cancellation scheduled for period end |
subscription.past_due |
Payment failed, retrying |
subscription.expired |
Subscription expired |
subscription.update |
Subscription updated |
subscription.trialing |
Subscription in trial |
subscription.paused |
Subscription paused |
refund.created |
Refund issued |
dispute.created |
Payment dispute created |
The component stores data in these sandboxed tables:
- customers - Maps Creem customers to your app's user IDs
- products - Product catalog synced from Creem
- subscriptions - Subscription state with real-time updates
- orders - One-time and recurring order records
- webhookEvents - Audit log of all webhook events (idempotent)
All data is automatically kept in sync via webhooks and is queryable with Convex's real-time reactivity.
Creem provides a test environment for development:
- Set
CREEM_ENVIRONMENT=test(this is the default) - Use test API keys from your Creem dashboard
- Test payments work with any card number
- Webhook events are sent to your test webhook URL
When ready for production, change to CREEM_ENVIRONMENT=production and use your live API keys.
MIT