# Stripe

Checkout, subscriptions and billing

- Package: @bijectionhq/stripe
- Category: Payments
- Status: Included
- License: Apache-2.0
- Bijection source (repository access required): https://github.com/bijectionhq/stripe/tree/22e23e2bdfcc88b48a1604a80bf60d8f15494360
- Upstream: https://github.com/convex/stripe/tree/5681a698cb59d96e87a2dc219904fc848b90ec9d
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/stripe

## Get the source

Bijection repository access is required. Request access at https://bijection.com/contact.

```sh
git clone https://github.com/bijectionhq/stripe.git
cd stripe
git checkout 22e23e2bdfcc88b48a1604a80bf60d8f15494360
```

## Before using this component

Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This source is included in Bijection's workspace. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.

## Upstream package documentation

Recorded 2026-10-23. This describes @convex-dev/stripe; check the Bijection adaptation separately.

A Convex component that handles the complete Stripe integration lifecycle including checkout sessions, subscription management, customer billing portals, and webhook synchronization. It automatically creates and manages customers, processes payments and subscriptions, and keeps Stripe data synchronized with your Convex database through webhook handlers. The component provides both a StripeSubscriptions client API and direct database queries for real-time payment data.

### Use cases

• **SaaS subscription billing** - Implement recurring billing with seat-based pricing, subscription upgrades/downgrades, and automatic customer portal access for plan management
• **One-time payment processing** - Handle product purchases, service fees, or donations with Stripe Checkout integration and payment status tracking
• **Team billing management** - Link subscriptions to organizations with quantity-based pricing, allowing teams to add or remove seats dynamically
• **Customer self-service billing** - Generate Customer Portal sessions so users can update payment methods, download invoices, and manage their billing without support
• **Multi-tenant payment tracking** - Query payments, subscriptions, and invoices in real-time across users and organizations with automatic webhook synchronization

### How it works

The component uses a StripeSubscriptions client that wraps Stripe API calls within Convex actions, handling customer creation, checkout sessions, and subscription management. You configure it by adding the component to your convex.config.ts and setting up webhook routes in http.ts that point to the component's webhook handler at /stripe/webhook.

Webhook events automatically synchronize Stripe data to dedicated Convex tables (customers, subscriptions, payments, invoices) in the component's namespace. The component handles 10+ webhook events including checkout.session.completed, customer.subscription.updated, and payment_intent.succeeded, maintaining real-time data consistency.

You access payment data through the component's public queries like listSubscriptionsByUserId and getPayment, or use the client methods like createCheckoutSession and updateSubscriptionQuantity in your actions. The component supports linking payments to both individual users and organizations through metadata, enabling flexible billing models.

### Upstream README

# @convex-dev/stripe

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

Requires Node.js 18 or later, matching the Stripe Node SDK v22 runtime
requirement.

## 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

```bash
npm install @convex-dev/stripe
```

### 2. Add to Your Convex App

Create or update `convex/convex.config.ts`:

```typescript
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](https://dashboard.convex.dev) → 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       |

### 4. Configure Stripe Webhooks

1. Go to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/test/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.deleted`
   - `customer.subscription.created`
   - `customer.subscription.updated`
   - `customer.subscription.deleted`
   - `invoice.created`
   - `invoice.finalized`
   - `invoice.updated`
   - `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`:

```typescript
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",
  apiVersion: "2026-04-22.dahlia", // Optional
});

export default http;
```

### 6. Use the Component

Create `convex/stripe.ts`:

```typescript
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

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

const stripeClient = new StripeSubscriptions(components.stripe, {
  STRIPE_SECRET_KEY: "sk_...", // Optional, defaults to process.env.STRIPE_SECRET_KEY
  apiVersion: "2026-04-22.dahlia", // Optional Stripe API version
});
```

#### Methods

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

### createCheckoutSession

```typescript
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
  params: {
    allow_promotion_codes: true,
    ui_mode: "embedded_page",
    return_url: "https://...",
  },                                // Optional Stripe Checkout Session params
});
```

`params` is a typed passthrough for Stripe Checkout Session fields that the
component does not model directly. Fields in `params` override constructed
defaults, except `mode`, which remains controlled by the top-level `mode`
argument. For non-hosted Checkout UI modes, the component omits `successUrl` and
`cancelUrl` from the Stripe request and expects redirect behavior to be supplied
through Stripe-supported params such as `return_url`.

### Component Queries

Access data directly via the component's public queries:

```typescript
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

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

## Webhook Events

The component automatically handles these Stripe webhook events:

| Event                           | Action                              |
| ------------------------------- | ----------------------------------- |
| `customer.created`              | Creates customer record             |
| `customer.updated`              | Updates customer record             |
| `customer.deleted`              | Scrubs customer PII                 |
| `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.finalized`             | Upserts invoice record              |
| `invoice.updated`               | Mirrors invoice metadata changes    |
| `invoice.paid`                  | Upserts invoice as paid             |
| `invoice.payment_failed`        | Marks invoice as failed             |
| `checkout.session.completed`    | Handles completed checkout sessions |

### Custom Webhook Handlers

Add custom logic to webhook events:

```typescript
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

| Field              | Type    | Description        |
| ------------------ | ------- | ------------------ |
| `stripeCustomerId` | string  | Stripe customer ID |
| `email`            | string? | Customer email     |
| `name`             | string? | Customer name      |
| `metadata`         | object? | Custom metadata    |

### subscriptions

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

### checkout_sessions

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

### payments

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

### invoices

| 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     |
| `metadata`             | object? | Invoice metadata  |

## Example App

Check out the full example app in the [`example/`](./example) directory:

```bash
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](https://clerk.com):

1. Create a Clerk application at [clerk.com](https://clerk.com)
2. Add `VITE_CLERK_PUBLISHABLE_KEY` to your `.env.local`
3. Create `convex/auth.config.ts`:

```typescript
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