← All packages

Payments · Convex component

convex-kinde-billing

Sync Kinde billing events into Convex reactively. Real-time subscription state, payment tracking, and metered usage.

npm install convex-kinde-billing
6downloads last week
1,682downloads last 12 months
v0.1.11latest, Mar 17, 2026
12releases since Mar 17, 2026

Downloads

Download history

Loading download history…

Build this

Build with convex-kinde-billing

The install and setup steps from the package documentation.

  1. 1

    Install

    bash
    npm install convex-kinde-billing

    Requirements: Convex v1.33.1 or later, Node.js 18+

  2. 2

    Quick Start

    Five steps to add Kinde billing to your Convex app.

    1. Add the component

    In convex/convex.config.ts:

    ts
    import { defineApp } from "convex/server";
    import convexKindeBilling from "convex-kinde-billing/convex.config";
    
    const app = defineApp();
    app.use(convexKindeBilling);
    
    export default app;

    2. Set environment variables

    bash
    npx convex env set KINDE_ISSUER_URL https://yourdomain.kinde.com

    3. Mount the webhook handler

    In convex/http.ts:

    ts
    import { httpRouter } from "convex/server";
    import { components } from "./_generated/api";
    import { KindeBilling } from "convex-kinde-billing";
    
    const kindeBilling = new KindeBilling(components.convexKindeBilling, {
      KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
    });
    
    const http = httpRouter();
    
    http.route({
      path: "/webhooks/kinde/billing",
      method: "POST",
      handler: kindeBilling.webhookHandler,
    });
    
    export default http;

    4. Register the webhook in Kinde

    1. In Kinde → Webhooks → Add endpoint
    2. Set the URL: https://your-deployment.convex.site/webhooks/kinde/billing
    3. Select all 8 billing events (listed in below)
    4. Save

    Your Convex site URL is in the Convex dashboard under Settings → URL & Deploy Key — it ends in .convex.site.

    5. Initialize the client

    In convex/billing.ts:

    ts
    import { components } from "./_generated/api";
    import { KindeBilling } from "convex-kinde-billing";
    
    export const kindeBilling = new KindeBilling(components.convexKindeBilling, {
      KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
    });

    Import kindeBilling from this file in any Convex function that needs billing.

  3. 3

    Setup

    convex/billing.ts — your central billing module:

    ts
    import { components } from "./_generated/api";
    import { KindeBilling } from "convex-kinde-billing";
    import { query } from "./_generated/server";
    import { v } from "convex/values";
    
    export const kindeBilling = new KindeBilling(components.convexKindeBilling, {
      KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
    });
    
    export const checkAccess = query({
      args: { customerId: v.string() },
      handler: async (ctx, { customerId }) =>
        kindeBilling.hasActivePlan(ctx, { customerId }),
    });

    convex/http.ts — webhook entry point:

    ts
    import { httpRouter } from "convex/server";
    import { components } from "./_generated/api";
    import { KindeBilling } from "convex-kinde-billing";
    
    const kindeBilling = new KindeBilling(components.convexKindeBilling, {
      KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
    });
    
    const http = httpRouter();
    
    http.route({
      path: "/webhooks/kinde/billing",
      method: "POST",
      handler: kindeBilling.webhookHandler,
    });
    
    export default http;
  4. 4

    Usage

    Check if a customer has an active plan

    ts
    export const checkAccess = query({
      args: { customerId: v.string() },
      handler: async (ctx, args) => {
        return await kindeBilling.hasActivePlan(ctx, { customerId: args.customerId });
      },
    });
    // Returns: true | false
    // Returns false (never throws) when customerId doesn't exist yet

    Check if a customer has a specific feature

    ts
    export const checkFeature = query({
      args: { customerId: v.string(), featureKey: v.string() },
      handler: async (ctx, args) => {
        return await kindeBilling.hasFeature(ctx, args);
      },
    });
    // Returns: true if customer is active and planId or planName contains featureKey

    Get the customer's current plan

    ts
    export const getPlan = query({
      args: { customerId: v.string() },
      handler: async (ctx, args) => {
        return await kindeBilling.getActivePlan(ctx, { customerId: args.customerId });
      },
    });
    // Returns: { planId, planName, status, currentPeriodEnd } | null

    Get the full subscription record

    ts
    export const getSubscription = query({
      args: { customerId: v.string() },
      handler: async (ctx, args) => {
        return await kindeBilling.getSubscription(ctx, { customerId: args.customerId });
      },
    });
    // Returns: Subscription | null

    List billing events for a customer

    ts
    export const getBillingHistory = query({
      args: { customerId: v.string() },
      handler: async (ctx, args) => {
        return await kindeBilling.listBillingEvents(ctx, {
          customerId: args.customerId,
          limit: 20,
        });
      },
    });
    // Returns: BillingEvent[] ordered newest first

    Query metered usage records

    ts
    export const getApiUsage = query({
      args: { customerId: v.string() },
      handler: async (ctx, args) => {
        return await kindeBilling.getUsage(ctx, {
          customerId: args.customerId,
          meterId: "api_calls",
          limit: 100,
        });
      },
    });
    // Returns: UsageRecord[] ordered newest first

    Gate a feature by plan status

    ts
    export const generateReport = action({
      args: { userId: v.string() },
      handler: async (ctx, { userId }) => {
        const active = await kindeBilling.hasActivePlan(ctx, { customerId: userId });
        if (!active) throw new Error("Upgrade required to generate reports.");
        // ... generate report
      },
    });

    Gate by plan name

    ts
    export const accessAdvancedAnalytics = query({
      args: { customerId: v.string() },
      handler: async (ctx, { customerId }) => {
        const plan = await kindeBilling.getActivePlan(ctx, { customerId });
        if (!plan || plan.planName !== "Pro") {
          return { allowed: false, reason: "Pro plan required" };
        }
        return { allowed: true };
      },
    });