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-billing6downloads 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
Install
bashnpm install convex-kinde-billingRequirements: Convex v1.33.1 or later, Node.js 18+
- 2
Quick Start
Five steps to add Kinde billing to your Convex app.
1. Add the component
In
convex/convex.config.ts:tsimport { 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
bashnpx convex env set KINDE_ISSUER_URL https://yourdomain.kinde.com3. Mount the webhook handler
In
convex/http.ts:tsimport { 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
- In Kinde → Webhooks → Add endpoint
- Set the URL:
https://your-deployment.convex.site/webhooks/kinde/billing - Select all 8 billing events (listed in below)
- 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:tsimport { 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
kindeBillingfrom this file in any Convex function that needs billing. - 3
Setup
convex/billing.ts— your central billing module:tsimport { 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:tsimport { 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
Usage
Check if a customer has an active plan
tsexport 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 yetCheck if a customer has a specific feature
tsexport 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 featureKeyGet the customer's current plan
tsexport const getPlan = query({ args: { customerId: v.string() }, handler: async (ctx, args) => { return await kindeBilling.getActivePlan(ctx, { customerId: args.customerId }); }, }); // Returns: { planId, planName, status, currentPeriodEnd } | nullGet the full subscription record
tsexport const getSubscription = query({ args: { customerId: v.string() }, handler: async (ctx, args) => { return await kindeBilling.getSubscription(ctx, { customerId: args.customerId }); }, }); // Returns: Subscription | nullList billing events for a customer
tsexport 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 firstQuery metered usage records
tsexport 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 firstGate a feature by plan status
tsexport 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
tsexport 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 }; }, });