
npm install @convex-dev/stripeA Convex component for integrating Stripe payments, subscriptions, and billing into your Convex application.
npm install @convex-dev/stripe
Create or update convex/convex.config.ts:
import { defineApp } from "convex/server";
import stripe from "@convex-dev/stripe/convex.config.js";
const app = defineApp();
app.use(stripe);
export default app;
Add these to your Convex Dashboard → Settings → Environment Variables:
| Variable | Description |
|---|---|
STRIPE_SECRET_KEY | Your Stripe secret key (sk_test_... or sk_live_...) |
STRIPE_WEBHOOK_SECRET | Webhook signing secret (whsec_...) - see Step 4 |
https://<your-convex-deployment>.convex.site/stripe/webhook
(Find your deployment name in the Convex dashboard - it's the part before .convex.cloud in your URL)checkout.session.completedcustomer.createdcustomer.updatedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.createdinvoice.finalizedinvoice.paidinvoice.payment_failedpayment_intent.succeededpayment_intent.payment_failedSTRIPE_WEBHOOK_SECRET in ConvexCreate convex/http.ts:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex-dev/stripe";
const http = httpRouter();
// Register Stripe webhook handler at /stripe/webhook
registerRoutes(http, components.stripe, {
webhookPath: "/stripe/webhook",
});
export default http;
Create convex/stripe.ts:
import { action } from "./_generated/server";
import { components } from "./_generated/api";
import { StripeSubscriptions } from "@convex-dev/stripe";
import { v } from "convex/values";
const stripeClient = new StripeSubscriptions(components.stripe, {});
// Create a checkout session for a subscription
export const createSubscriptionCheckout = action({
args: { priceId: v.string() },
returns: v.object({
sessionId: v.string(),
url: v.union(v.string(), v.null()),
}),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
// Get or create a Stripe customer
const customer = await stripeClient.getOrCreateCustomer(ctx, {
userId: identity.subject,
email: identity.email,
name: identity.name,
});
// Create checkout session
return await stripeClient.createCheckoutSession(ctx, {
priceId: args.priceId,
customerId: customer.customerId,
mode: "subscription",
successUrl: "http://localhost:5173/?success=true",
cancelUrl: "http://localhost:5173/?canceled=true",
subscriptionMetadata: { userId: identity.subject },
});
},
});
// Create a checkout session for a one-time payment
export const createPaymentCheckout = action({
args: { priceId: v.string() },
returns: v.object({
sessionId: v.string(),
url: v.union(v.string(), v.null()),
}),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const customer = await stripeClient.getOrCreateCustomer(ctx, {
userId: identity.subject,
email: identity.email,
name: identity.name,
});
return await stripeClient.createCheckoutSession(ctx, {
priceId: args.priceId,
customerId: customer.customerId,
mode: "payment",
successUrl: "http://localhost:5173/?success=true",
cancelUrl: "http://localhost:5173/?canceled=true",
paymentIntentMetadata: { userId: identity.subject },
});
},
});
import { StripeSubscriptions } from "@convex-dev/stripe";
const stripeClient = new StripeSubscriptions(components.stripe, {
STRIPE_SECRET_KEY: "sk_...", // Optional, defaults to process.env.STRIPE_SECRET_KEY
});
| Method | Description |
|---|---|
createCheckoutSession() | Create a Stripe Checkout session |
createCustomerPortalSession() | Generate a Customer Portal URL |
createCustomer() | Create a new Stripe customer |
getOrCreateCustomer() | Get existing or create new customer |
cancelSubscription() | Cancel a subscription |
reactivateSubscription() | Reactivate a subscription set to cancel |
updateSubscriptionQuantity() | Update seat count |
await stripeClient.createCheckoutSession(ctx, {
priceId: "price_...",
customerId: "cus_...", // Optional
mode: "subscription", // "subscription" | "payment" | "setup"
successUrl: "https://...",
cancelUrl: "https://...",
quantity: 1, // Optional, default 1
metadata: {}, // Optional, session metadata
subscriptionMetadata: {}, // Optional, attached to subscription
paymentIntentMetadata: {}, // Optional, attached to payment intent
});
Access data directly via the component's public queries:
import { query } from "./_generated/server";
import { components } from "./_generated/api";
// List subscriptions for a user
export const getUserSubscriptions = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
return await ctx.runQuery(
components.stripe.public.listSubscriptionsByUserId,
{ userId: identity.subject },
);
},
});
// List payments for a user
export const getUserPayments = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
return await ctx.runQuery(components.stripe.public.listPaymentsByUserId, {
userId: identity.subject,
});
},
});
| Query | Arguments | Description |
|---|---|---|
getCustomer | stripeCustomerId | Get a customer by Stripe ID |
listSubscriptions | stripeCustomerId | List subscriptions for a customer |
listSubscriptionsByUserId | userId | List subscriptions for a user |
getSubscription | stripeSubscriptionId | Get a subscription by ID |
getSubscriptionByOrgId | orgId | Get subscription for an org |
getPayment | stripePaymentIntentId | Get a payment by ID |
listPayments | stripeCustomerId | List payments for a customer |
listPaymentsByUserId | userId | List payments for a user |
listPaymentsByOrgId | orgId | List payments for an org |
listInvoices | stripeCustomerId | List invoices for a customer |
listInvoicesByUserId | userId | List invoices for a user |
listInvoicesByOrgId | orgId | List invoices for an org |
The component automatically handles these Stripe webhook events:
| Event | Action |
|---|---|
customer.created | Creates customer record |
customer.updated | Updates customer record |
customer.subscription.created | Creates subscription record |
customer.subscription.updated | Updates subscription record |
customer.subscription.deleted | Marks subscription as canceled |
payment_intent.succeeded | Creates payment record |
payment_intent.payment_failed | Updates payment status |
invoice.created | Creates invoice record |
invoice.paid | Updates invoice to paid |
invoice.payment_failed | Marks invoice as failed |
checkout.session.completed | Handles completed checkout sessions |
Add custom logic to webhook events:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex-dev/stripe";
import type Stripe from "stripe";
const http = httpRouter();
registerRoutes(http, components.stripe, {
events: {
"customer.subscription.updated": async (ctx, event: Stripe.CustomerSubscriptionUpdatedEvent) => {
const subscription = event.data.object;
console.log("Subscription updated:", subscription.id, subscription.status);
// Add custom logic here
},
},
onEvent: async (ctx, event: Stripe.Event) => {
// Called for ALL events - useful for logging/analytics
console.log("Stripe event:", event.type);
},
});
export default http;
The component creates these tables in its namespace:
| Field | Type | Description |
|---|---|---|
stripeCustomerId | string | Stripe customer ID |
email | string? | Customer email |
name | string? | Customer name |
metadata | object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripeSubscriptionId | string | Stripe subscription ID |
stripeCustomerId | string | Customer ID |
status | string | Subscription status |
priceId | string | Price ID |
quantity | number? | Seat count |
currentPeriodEnd | number | Period end timestamp |
cancelAtPeriodEnd | boolean | Will cancel at period end |
userId | string? | Linked user ID |
orgId | string? | Linked org ID |
metadata | object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripeCheckoutSessionId | string | Checkout session ID |
stripeCustomerId | string? | Customer ID |
status | string | Session status |
mode | string | Session mode (payment/subscription/setup) |
metadata | object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripePaymentIntentId | string | Payment intent ID |
stripeCustomerId | string? | Customer ID |
amount | number | Amount in cents |
currency | string | Currency code |
status | string | Payment status |
created | number | Created timestamp |
userId | string? | Linked user ID |
orgId | string? | Linked org ID |
metadata | object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripeInvoiceId | string | Invoice ID |
stripeCustomerId | string | Customer ID |
stripeSubscriptionId | string? | Subscription ID |
status | string | Invoice status |
amountDue | number | Amount due |
amountPaid | number | Amount paid |
created | number | Created timestamp |
userId | string? | Linked user ID |
orgId | string? | Linked org ID |
Check out the full example app in the example/ directory:
git clone https://github.com/get-convex/convex-stripe
cd convex-stripe
npm install
npm run dev
The example includes:
This component works with any Convex authentication provider. The example uses Clerk:
VITE_CLERK_PUBLISHABLE_KEY to your .env.localconvex/auth.config.ts:export default {
providers: [
{
domain: "https://your-clerk-domain.clerk.accounts.dev",
applicationID: "convex",
},
],
};
Make sure you've:
STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in Convex environment
variablesinvoice.created and invoice.finalized events (not just
invoice.paid)Ensure your auth provider is configured:
convex/auth.config.ts with your providernpx convex dev to push the configCheck the Convex logs in your dashboard for errors. Common issues:
STRIPE_WEBHOOK_SECREThttps://<deployment>.convex.site/stripe/webhook)Apache-2.0