RealtimeKeep your app up to date
AuthenticationOver 80+ OAuth integrations
Convex Components
ComponentsIndependent, modular, TypeScript building blocks for your backend.
Open sourceSelf host and develop locally
AI CodingGenerate high quality Convex code with AI
Compare
Convex vs. Firebase
Convex vs. Supabase
Convex vs. SQL
DocumentationGet started with your favorite frameworks
SearchSearch across Docs, Stack, and Discord
TemplatesUse a recipe to get started quickly
Convex for StartupsStart and scale your company with Convex
Convex ChampionsAmbassadors that support our thriving community
Convex CommunityShare ideas and ask for help in our community Discord
Stack
Stack

Stack is the Convex developer portal and blog, sharing bright ideas and techniques for building with Convex.

Explore Stack
BlogDocsPricing
GitHub
Log inStart building
Back to Components

Stripe

get-convex's avatar
get-convex/stripe
View repo
GitHub logoView package

Category

Payments
Stripe hero image
npm install @convex-dev/stripe

@convex-dev/stripe

A Convex component for integrating Stripe payments, subscriptions, and billing into your Convex application.

npm version

Features#

  • 🛒 Checkout Sessions - Create one-time payment and subscription checkouts
  • 📦 Subscription Management - Create, update, cancel subscriptions
  • 👥 Customer Management - Automatic customer creation and linking
  • 💳 Customer Portal - Let users manage their billing
  • 🪑 Seat-Based Pricing - Update subscription quantities for team billing
  • 🔗 User/Org Linking - Link payments and subscriptions to users or organizations
  • 🔔 Webhook Handling - Automatic sync of Stripe data to your Convex database
  • 📊 Real-time Data - Query payments, subscriptions, invoices in real-time

Quick Start#

1. Install the Component#

npm install @convex-dev/stripe

2. Add to Your Convex App#

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;

3. Set Up Environment Variables#

Add these to your Convex Dashboard → Settings → Environment Variables:

VariableDescription
STRIPE_SECRET_KEYYour Stripe secret key (sk_test_... or sk_live_...)
STRIPE_WEBHOOK_SECRETWebhook signing secret (whsec_...) - see Step 4

4. Configure Stripe Webhooks#

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click "Add endpoint"
  3. Enter your webhook URL:
    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)

  4. Select these events:
    • checkout.session.completed
    • customer.created
    • customer.updated
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.created
    • invoice.finalized
    • invoice.paid
    • invoice.payment_failed
    • payment_intent.succeeded
    • payment_intent.payment_failed
  5. Click "Add endpoint"
  6. Copy the Signing secret and add it as STRIPE_WEBHOOK_SECRET in Convex

5. Register Webhook Routes#

Create 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;

6. Use the Component#

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 },
    });
  },
});

API Reference#

StripeSubscriptions Client#

import { StripeSubscriptions } from "@convex-dev/stripe";

const stripeClient = new StripeSubscriptions(components.stripe, {
  STRIPE_SECRET_KEY: "sk_...", // Optional, defaults to process.env.STRIPE_SECRET_KEY
});

Methods#

MethodDescription
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

createCheckoutSession#

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
});

Component Queries#

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,
    });
  },
});

Available Public Queries#

QueryArgumentsDescription
getCustomerstripeCustomerIdGet a customer by Stripe ID
listSubscriptionsstripeCustomerIdList subscriptions for a customer
listSubscriptionsByUserIduserIdList subscriptions for a user
getSubscriptionstripeSubscriptionIdGet a subscription by ID
getSubscriptionByOrgIdorgIdGet subscription for an org
getPaymentstripePaymentIntentIdGet a payment by ID
listPaymentsstripeCustomerIdList payments for a customer
listPaymentsByUserIduserIdList payments for a user
listPaymentsByOrgIdorgIdList payments for an org
listInvoicesstripeCustomerIdList invoices for a customer
listInvoicesByUserIduserIdList invoices for a user
listInvoicesByOrgIdorgIdList invoices for an org

Webhook Events#

The component automatically handles these Stripe webhook events:

EventAction
customer.createdCreates customer record
customer.updatedUpdates customer record
customer.subscription.createdCreates subscription record
customer.subscription.updatedUpdates subscription record
customer.subscription.deletedMarks subscription as canceled
payment_intent.succeededCreates payment record
payment_intent.payment_failedUpdates payment status
invoice.createdCreates invoice record
invoice.paidUpdates invoice to paid
invoice.payment_failedMarks invoice as failed
checkout.session.completedHandles completed checkout sessions

Custom Webhook Handlers#

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;

Database Schema#

The component creates these tables in its namespace:

customers#

FieldTypeDescription
stripeCustomerIdstringStripe customer ID
emailstring?Customer email
namestring?Customer name
metadataobject?Custom metadata

subscriptions#

FieldTypeDescription
stripeSubscriptionIdstringStripe subscription ID
stripeCustomerIdstringCustomer ID
statusstringSubscription status
priceIdstringPrice ID
quantitynumber?Seat count
currentPeriodEndnumberPeriod end timestamp
cancelAtPeriodEndbooleanWill cancel at period end
userIdstring?Linked user ID
orgIdstring?Linked org ID
metadataobject?Custom metadata

checkout_sessions#

FieldTypeDescription
stripeCheckoutSessionIdstringCheckout session ID
stripeCustomerIdstring?Customer ID
statusstringSession status
modestringSession mode (payment/subscription/setup)
metadataobject?Custom metadata

payments#

FieldTypeDescription
stripePaymentIntentIdstringPayment intent ID
stripeCustomerIdstring?Customer ID
amountnumberAmount in cents
currencystringCurrency code
statusstringPayment status
creatednumberCreated timestamp
userIdstring?Linked user ID
orgIdstring?Linked org ID
metadataobject?Custom metadata

invoices#

FieldTypeDescription
stripeInvoiceIdstringInvoice ID
stripeCustomerIdstringCustomer ID
stripeSubscriptionIdstring?Subscription ID
statusstringInvoice status
amountDuenumberAmount due
amountPaidnumberAmount paid
creatednumberCreated timestamp
userIdstring?Linked user ID
orgIdstring?Linked org ID

Example App#

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:

  • Landing page with product showcase
  • One-time payments and subscriptions
  • User profile with order history
  • Subscription management (cancel, update seats)
  • Customer portal integration
  • Team/organization billing

Authentication#

This component works with any Convex authentication provider. The example uses Clerk:

  1. Create a Clerk application at clerk.com
  2. Add VITE_CLERK_PUBLISHABLE_KEY to your .env.local
  3. Create convex/auth.config.ts:
export default {
  providers: [
    {
      domain: "https://your-clerk-domain.clerk.accounts.dev",
      applicationID: "convex",
    },
  ],
};

Troubleshooting#

Tables are empty after checkout#

Make sure you've:

  1. Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in Convex environment variables
  2. Configured the webhook endpoint in Stripe with the correct events
  3. Added invoice.created and invoice.finalized events (not just invoice.paid)

"Not authenticated" errors#

Ensure your auth provider is configured:

  1. Create convex/auth.config.ts with your provider
  2. Run npx convex dev to push the config
  3. Verify the user is signed in before calling actions

Webhooks returning 400/500#

Check the Convex logs in your dashboard for errors. Common issues:

  • Missing STRIPE_WEBHOOK_SECRET
  • Wrong webhook URL (should be https://<deployment>.convex.site/stripe/webhook)
  • Missing events in webhook configuration

License#

Apache-2.0

Get your app up and running in minutes
Start building
Convex logo
ProductSyncRealtimeAuthOpen sourceAI codingChefFAQPricing
DevelopersDocsBlogComponentsTemplatesStartupsChampionsChangelogPodcastLLMs.txt
CompanyAbout usBrandInvestorsBecome a partnerJobsNewsEventsTerms of servicePrivacy policySecurity
SocialTwitterDiscordYouTubeLumaLinkedInGitHub
A Trusted Solution
  • SOC 2 Type II Compliant
  • HIPAA Compliant
  • GDPR Verified
©2026 Convex, Inc.