@crovver/sdk
v0.1.0
Published
Crovver SDK for subscription management
Readme
Crovver SDK
Official TypeScript/JavaScript SDK for integrating Crovver subscription management into your SaaS application.
Features
✅ Complete API Coverage - All 7 public endpoints implemented
✅ TypeScript Support - Full type definitions included
✅ B2B & D2C Support - Works with both organization types
✅ Error Handling - Built-in error handling with custom error types
✅ Entitlement Checks - Feature access and usage limit verification
✅ Subscription Management - Create checkouts, manage subscriptions
✅ Tenant Management - Create and manage tenants (B2B)
Installation
npm install @crovver/sdk
# or
yarn add @crovver/sdk
# or
pnpm add @crovver/sdkQuick Start
import { CrovverClient } from "@crovver/sdk";
// Initialize the client
const crovver = new CrovverClient({
apiKey: "your-api-key-here",
baseUrl: "https://your-crovver-instance.com", // Optional, defaults to localhost:3000
timeout: 30000, // Optional, defaults to 30000ms
});
// Check if a user can access a feature
const canAccess = await crovver.canAccess("tenant-123", "advanced-analytics");
if (canAccess) {
// Grant access to the feature
console.log("Access granted!");
}API Reference
Configuration
interface CrovverConfig {
apiKey: string; // Your organization's API key (required)
baseUrl?: string; // Crovver instance URL (optional)
timeout?: number; // Request timeout in ms (optional)
}Error Handling
The SDK throws CrovverError for all API errors:
import { CrovverClient, CrovverError } from "@crovver/sdk";
try {
await crovver.canAccess("tenant-123", "premium-feature");
} catch (error) {
if (error instanceof CrovverError) {
console.error("Error:", error.message);
console.error("Status Code:", error.statusCode);
console.error("Original Error:", error.originalError);
}
}🏢 Tenant Management (B2B Only)
Create Tenant
B2B organizations only. For D2C organizations, tenants are automatically created during checkout.
const result = await crovver.createTenant({
externalTenantId: "company-123", // Your app's company/workspace ID
name: "Acme Corporation", // Company name
slug: "acme-corp", // URL-friendly slug (optional)
ownerExternalUserId: "user-456", // Owner's user ID from your app
ownerEmail: "[email protected]", // Owner's email (optional)
ownerName: "John Doe", // Owner's name (optional)
metadata: {
// Optional metadata
industry: "Technology",
size: "50-100",
},
});
console.log("Tenant created:", result.tenant.id);
console.log("Owner:", result.owner.id);Response:
{
tenant: {
id: "uuid",
externalTenantId: "company-123",
name: "Acme Corporation",
slug: "acme-corp",
isActive: true,
metadata: { ... },
createdAt: "2026-02-02T10:00:00Z"
},
owner: {
id: "uuid",
externalUserId: "user-456",
email: "[email protected]",
name: "John Doe"
},
message: "Tenant created successfully"
}Get Tenant
const tenant = await crovver.getTenant("company-123");💳 Plans
Get All Plans
Fetch all available plans for your organization. Use this to render pricing pages.
const { data } = await crovver.getPlans();
console.log(`Found ${data.plans.length} plans`);
data.plans.forEach((plan) => {
console.log(
`${plan.name}: ${plan.pricing.currency} ${plan.pricing.amount}/${plan.pricing.interval}`
);
console.log("Features:", plan.features);
console.log("Limits:", plan.limits);
});Response:
{
success: true,
data: {
plans: [
{
id: "plan-uuid",
name: "Pro Plan",
description: "For growing teams",
pricing: {
amount: 99,
currency: "USD",
interval: "month"
},
trial: {
days: 14,
available: true
},
features: {
"advanced-analytics": true,
"priority-support": true,
"custom-branding": false
},
limits: {
"api-calls": 10000,
"storage-gb": 100,
"team-members": 25
},
product: {
id: "product-uuid",
name: "My SaaS Product",
description: "..."
},
payment_providers: [...],
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z"
}
],
organization: {
id: "org-uuid",
name: "My Organization",
type: "b2b"
}
}
}📊 Subscriptions
Get Subscriptions
Fetch active subscriptions for a tenant (B2B) or user (D2C).
// B2B: Use external tenant ID
const { data } = await crovver.getSubscriptions("company-123");
// D2C: Use external user ID
const { data } = await crovver.getSubscriptions("user-789");
console.log(
`${data.tenant.name} has ${data.subscriptions.length} subscriptions`
);
data.subscriptions.forEach((sub) => {
console.log(`Plan: ${sub.plan.name}`);
console.log(`Status: ${sub.status}`);
console.log(`Next billing: ${sub.billing.next_billing_date}`);
if (sub.trial.is_active) {
console.log(`Trial ends in ${sub.trial.days_remaining} days`);
}
if (sub.cancellation.is_canceled) {
console.log(`Will end on: ${sub.cancellation.will_end_at}`);
}
});Response:
{
success: true,
data: {
subscriptions: [
{
id: "sub-uuid",
status: "active",
provider_subscription_id: "stripe_sub_123",
billing: {
current_period_start: "2026-01-01T00:00:00Z",
current_period_end: "2026-02-01T00:00:00Z",
next_billing_date: "2026-02-01T00:00:00Z"
},
trial: {
is_active: false,
ends_at: null,
days_remaining: 0
},
cancellation: {
is_canceled: false,
canceled_at: null,
will_end_at: null
},
plan: { ... },
product: { ... },
payment_providers: [...],
metadata: {},
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z"
}
],
tenant: {
id: "tenant-uuid",
external_tenant_id: "company-123",
name: "Acme Corporation",
slug: "acme-corp",
metadata: {}
},
organization: { ... }
}
}🛒 Checkout
Create Checkout Session
Create a payment checkout session. Redirects the user to the payment provider.
B2B Example:
const checkout = await crovver.createCheckoutSession({
requestingUserId: "user-456", // User making the purchase
requestingTenantId: "company-123", // Tenant (required for B2B)
planId: "plan-uuid",
provider: "stripe", // 'stripe' | 'khalti' | 'esewa'
successUrl: "https://myapp.com/success",
cancelUrl: "https://myapp.com/cancel",
metadata: {
source: "pricing-page",
coupon: "SAVE20",
},
});
// Redirect user to checkout
window.location.href = checkout.checkoutUrl;D2C Example:
// For D2C, tenant is auto-created - provide user details
const checkout = await crovver.createCheckoutSession({
requestingUserId: "user-789",
userEmail: "[email protected]", // Required for D2C
userName: "John Doe", // Required for D2C
planId: "plan-uuid",
provider: "stripe",
successUrl: "https://myapp.com/success",
cancelUrl: "https://myapp.com/cancel",
});
window.location.href = checkout.checkoutUrl;Response:
{
subscriptionId: "sub-uuid",
checkoutUrl: "https://checkout.stripe.com/...",
sessionId: "cs_test_..."
}🔐 Entitlement Checks
Check Feature Access
Verify if a tenant/user has access to a specific feature.
// B2B: Check tenant access
const canAccess = await crovver.canAccess("company-123", "advanced-analytics");
if (canAccess) {
// Show the feature
console.log("Feature enabled!");
} else {
// Show upgrade prompt
console.log("Please upgrade to access this feature");
}
// D2C: Check user access
const canExport = await crovver.canAccess("user-789", "export-data");Returns: boolean
📈 Usage Tracking
Record Usage
Track usage for metered features.
// Record API call
await crovver.recordUsage(
"company-123", // Entity ID
"api-calls", // Metric key
1, // Value (default: 1)
{
// Metadata (optional)
endpoint: "/api/v1/users",
method: "GET",
responseTime: 45,
}
);
// Record storage usage
await crovver.recordUsage("company-123", "storage-gb", 5.2);
// Record custom event (increment by 1)
await crovver.recordUsage("company-123", "reports-generated");Check Usage Limits
Check current usage against limits before allowing an action.
const usage = await crovver.checkUsageLimit("company-123", "api-calls");
console.log(`Used: ${usage.current} / ${usage.limit}`);
console.log(`Remaining: ${usage.remaining}`);
console.log(`Percentage: ${usage.percentage}%`);
if (!usage.allowed) {
// Show upgrade prompt or limit reached message
console.log("Usage limit reached! Please upgrade your plan.");
} else {
// Allow the action
await performApiCall();
// Record the usage
await crovver.recordUsage("company-123", "api-calls");
}Response:
{
allowed: true,
current: 7500,
limit: 10000,
remaining: 2500,
percentage: 75
}🎯 Common Patterns
Feature Gate Pattern
async function withFeatureGate(
entityId: string,
feature: string,
action: () => Promise<void>
) {
const canAccess = await crovver.canAccess(entityId, feature);
if (!canAccess) {
throw new Error(`Feature '${feature}' not available on your plan`);
}
await action();
}
// Usage
await withFeatureGate("company-123", "advanced-analytics", async () => {
// Your feature code here
await generateAdvancedReport();
});Usage Limit Guard
async function withUsageCheck(
entityId: string,
metric: string,
action: () => Promise<void>
) {
const usage = await crovver.checkUsageLimit(entityId, metric);
if (!usage.allowed) {
throw new Error(`Usage limit reached for '${metric}'`);
}
await action();
// Record the usage after successful action
await crovver.recordUsage(entityId, metric);
}
// Usage
await withUsageCheck("company-123", "api-calls", async () => {
await makeApiCall();
});Subscription Status Check
async function requireActiveSubscription(entityId: string) {
const { data } = await crovver.getSubscriptions(entityId);
const hasActive = data.subscriptions.some(
(sub) => sub.status === "active" || sub.status === "trial"
);
if (!hasActive) {
throw new Error("No active subscription found");
}
return data.subscriptions[0]; // Return the first active subscription
}🔄 Organization Types
Crovver supports two organization types:
B2B (Business-to-Business)
- Tenant-centric: Multiple tenants (companies/workspaces) per organization
- Manual tenant creation: Call
createTenant()when a company signs up - Subscriptions belong to tenants: Each tenant has its own subscription
- Use
requestingTenantId: Pass tenant ID for entitlement checks
// Create tenant first
await crovver.createTenant({
externalTenantId: "company-123",
name: "Acme Corp",
ownerExternalUserId: "user-456",
});
// Then check access using tenant ID
const canAccess = await crovver.canAccess("company-123", "feature");D2C (Direct-to-Consumer)
- User-centric: One tenant per user
- Auto tenant creation: Tenant created automatically during checkout
- Subscriptions belong to users: Each user has their own subscription
- Use
requestingUserId: Pass user ID for entitlement checks
// No manual tenant creation needed
// Just create checkout - tenant auto-created
const checkout = await crovver.createCheckoutSession({
requestingUserId: "user-789",
userEmail: "[email protected]",
userName: "John Doe",
planId: "plan-uuid",
provider: "stripe",
successUrl: "https://myapp.com/success",
cancelUrl: "https://myapp.com/cancel",
});
// Check access using user ID
const canAccess = await crovver.canAccess("user-789", "feature");🛠️ Framework Integration Examples
Next.js API Route
// app/api/check-feature/route.ts
import { CrovverClient } from "@crovver/sdk";
import { NextResponse } from "next/server";
const crovver = new CrovverClient({
apiKey: process.env.CROVVER_API_KEY!,
baseUrl: process.env.CROVVER_BASE_URL,
});
export async function POST(request: Request) {
const { tenantId, feature } = await request.json();
try {
const canAccess = await crovver.canAccess(tenantId, feature);
return NextResponse.json({ canAccess });
} catch (error) {
return NextResponse.json(
{ error: "Failed to check feature access" },
{ status: 500 }
);
}
}Express.js Middleware
// middleware/checkFeature.ts
import { CrovverClient, CrovverError } from "@crovver/sdk";
import { Request, Response, NextFunction } from "express";
const crovver = new CrovverClient({
apiKey: process.env.CROVVER_API_KEY!,
baseUrl: process.env.CROVVER_BASE_URL,
});
export function requireFeature(featureKey: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const tenantId = req.user?.tenantId; // Your auth logic
if (!tenantId) {
return res.status(401).json({ error: "Unauthorized" });
}
try {
const canAccess = await crovver.canAccess(tenantId, featureKey);
if (!canAccess) {
return res.status(403).json({
error: "Feature not available",
message: `Your plan does not include ${featureKey}`,
});
}
next();
} catch (error) {
if (error instanceof CrovverError) {
return res.status(error.statusCode || 500).json({
error: error.message,
});
}
next(error);
}
};
}
// Usage
app.get("/api/analytics", requireFeature("advanced-analytics"), (req, res) => {
// Your analytics endpoint
});React Hook
// hooks/useCrovver.ts
import { useState, useEffect } from "react";
import { CrovverClient } from "@crovver/sdk";
const crovver = new CrovverClient({
apiKey: process.env.NEXT_PUBLIC_CROVVER_API_KEY!,
baseUrl: process.env.NEXT_PUBLIC_CROVVER_BASE_URL,
});
export function useFeatureAccess(tenantId: string, feature: string) {
const [canAccess, setCanAccess] = useState<boolean>(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function checkAccess() {
try {
setLoading(true);
const access = await crovver.canAccess(tenantId, feature);
if (!cancelled) {
setCanAccess(access);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
checkAccess();
return () => {
cancelled = true;
};
}, [tenantId, feature]);
return { canAccess, loading, error };
}
// Usage in component
function AnalyticsPage() {
const { canAccess, loading } = useFeatureAccess(
"company-123",
"advanced-analytics"
);
if (loading) return <div>Loading...</div>;
if (!canAccess) {
return <UpgradePrompt feature="advanced-analytics" />;
}
return <AdvancedAnalyticsDashboard />;
}📝 TypeScript Support
The SDK is written in TypeScript and includes full type definitions. All request and response types are exported:
import {
CrovverClient,
CrovverError,
CrovverConfig,
CreateTenantRequest,
CreateTenantResponse,
GetPlansResponse,
GetSubscriptionsResponse,
CreateCheckoutSessionRequest,
CreateCheckoutSessionResponse,
CheckUsageLimitResponse,
Plan,
Subscription,
Tenant,
} from "@crovver/sdk";🐛 Error Handling
All errors thrown by the SDK are instances of CrovverError:
class CrovverError extends Error {
statusCode?: number; // HTTP status code (if available)
originalError?: any; // Original error object
}Common Error Codes:
401- Invalid API key or unauthorized403- Forbidden (e.g., D2C org trying to create tenant manually)404- Resource not found (tenant, subscription, etc.)409- Conflict (e.g., duplicate tenant)500- Internal server error
🤝 Support
- Documentation: https://docs.crovver.com
- Issues: GitHub Issues
- Email: [email protected]
📄 License
MIT License - see LICENSE file for details
