@chatarmin/os
v1.5.1
Published
Type-safe SDK for ChatarminOS - Customer & Subscription Management
Readme
@chatarmin/os
Type-safe SDK for ChatarminOS — the all-in-one platform for B2B SaaS to manage customers, subscriptions, feature access, and billing.
Features
- 🏢 Company Management — Create, link, and manage customer companies
- 🎫 Subscription Tiers — Define tiers with features and Stripe pricing
- ✨ Feature Access — Check entitlements and enforce usage limits
- 📊 Usage Tracking — Metered billing with idempotency support
- 💳 Stripe Integration — Claim checkout sessions, link subscriptions
- 🔗 Smart Linking — Intelligent company matching by domain/name/email
- 👥 Contact Sync — Bulk upsert contacts for each company
- 🚀 Unified Onboarding — Complete setup in a single API call
Table of Contents
Installation
pnpm add @chatarmin/os
# or
npm install @chatarmin/os
# or
yarn add @chatarmin/osQuick Start
import { ChatarminOS } from "@chatarmin/os"
const os = new ChatarminOS({
apiKey: process.env.OS_API_KEY!,
// Optional: custom base URL for self-hosted
// baseUrl: 'https://your-os.example.com/api/v1'
})
// Onboard a new customer (single call does everything!)
const result = await os.onboard({
externalOrgId: "org_abc123", // Your product's org ID
label: "My Product", // Display label for the product link (required)
hints: {
companyName: "Acme Inc",
domain: "acme.com",
},
tierCode: "free",
contactEmail: "[email protected]",
contactName: "John Doe",
})
console.log(result.companyId) // Use this for all future API calls
console.log(result.linkStatus) // 'created' | 'linked' | 'already_linked'API Reference
Companies
Manage customer companies and their links to your product.
companies.getByProductLink(input)
Look up a company by your external organization ID.
const company = await os.companies.getByProductLink({
externalOrgId: "org_abc123",
})
if (company) {
console.log(`Found: ${company.name} (${company.id})`)
} else {
console.log("Company not linked yet")
}companies.create(input)
Create a new company and link it to your product.
const company = await os.companies.create({
name: "Acme Inc",
domain: "acme.com",
externalOrgId: "org_abc123",
label: "My Product", // Display label for the product link
contactEmail: "[email protected]",
contactName: "John Doe",
createdAt: "2024-01-15T10:30:00.000Z", // Optional: historical date
})
console.log(company.id) // UUID
console.log(company.shortId) // e.g., "acme-inc"companies.update(input)
Update company metadata (e.g., backfill historical dates).
await os.companies.update({
companyId: "uuid-here",
createdAt: "2023-06-01T00:00:00.000Z",
})companies.smartLink(input)
Intelligently find and link a company using hints.
const result = await os.companies.smartLink({
externalOrgId: "org_abc123",
label: "My Product", // Display label for the product link (required)
hints: {
companyName: "Acme Inc",
domain: "acme.com",
emails: ["[email protected]", "[email protected]"],
},
minConfidence: 0.7, // Default: 0.8
})
switch (result.status) {
case "already_linked":
console.log("Already linked to your product")
break
case "linked":
console.log(
`Auto-linked to ${result.customerName} (${result.confidence}% confidence)`
)
break
case "suggestions":
console.log("Multiple matches found:", result.suggestions)
break
case "no_match":
console.log("No matching company found")
break
}Matching priority:
- Domain match → 95% confidence
- Name match → up to 80% confidence
- Email domain match → 70% confidence
Contacts
Manage people associated with companies.
contacts.create(input)
Create or update a single contact.
const contact = await os.contacts.create({
companyId: "comp_xxx",
email: "[email protected]",
name: "John Doe",
role: "CEO",
isPrimary: true,
avatarUrl: "https://example.com/avatar.jpg",
metadata: { slackId: "U123" },
})
if (contact.isNew) {
console.log("Created new contact")
} else {
console.log("Updated existing contact")
}contacts.bulkUpsert(input)
Bulk create or update multiple contacts. Recommended for onboarding.
const result = await os.contacts.bulkUpsert({
companyId: "comp_xxx",
contacts: [
{ email: "[email protected]", name: "Alice", role: "CEO", isPrimary: true },
{ email: "[email protected]", name: "Bob", role: "CTO" },
{ email: "[email protected]", name: "Charlie", role: "Engineer" },
],
})
console.log(`Created: ${result.created}, Updated: ${result.updated}`)
// result.contacts contains individual results with IDscontacts.list(companyId, primaryOnly?)
List contacts for a company.
// Get all contacts
const contacts = await os.contacts.list("comp_xxx")
// Get only the primary contact
const [primary] = await os.contacts.list("comp_xxx", true)Features
Check and manage feature access for companies.
features.check(input)
Check if a company has access to a specific feature.
const access = await os.features.check({
companyId: "comp_xxx",
featureCode: "ai_credit",
})
console.log({
enabled: access.isEnabled, // Feature is turned on
canUse: access.canUse, // Can use right now (not at limit)
hasQuantity: access.hasQuantity, // Is a quantity-based feature
usage: access.currentUsage, // Current usage count
limit: access.quantityLimit, // Max allowed (null = unlimited)
included: access.includedQuantity, // Free tier before billing
remaining: access.remaining, // How many left (null = unlimited)
})
if (!access.canUse) {
throw new Error("Upgrade required")
}features.listAccess(companyId)
List all features and their access status for a company.
const features = await os.features.listAccess("comp_xxx")
for (const f of features) {
console.log(`${f.featureName}: ${f.isEnabled ? "✓" : "✗"}`)
if (f.quantityLimit) {
console.log(` Usage: ${f.currentUsage} / ${f.quantityLimit}`)
}
}features.setAccess(input)
Update feature access with partial config merge.
// Update by company ID
await os.features.setAccess({
companyId: "comp_xxx",
featureCode: "ai_credit",
isEnabled: true,
quantityLimit: 1000,
includedQuantity: 100,
config: {
model: "gpt-4",
maxTokens: 4096,
},
source: "manual", // 'subscription' | 'manual' | 'trial' | 'api'
validUntil: new Date("2025-12-31"), // null for no expiration
})
// Update by external org ID
await os.features.setAccess({
externalOrgId: "org_abc123",
featureCode: "team_seats",
config: { maxSeats: 50 }, // Only updates maxSeats, preserves other config
})features.getAccessByExternalOrgId(externalOrgId)
Get all features using your product's external org ID.
const result = await os.features.getAccessByExternalOrgId("org_abc123")
console.log(`Company: ${result.companyId}`)
for (const f of result.features) {
console.log(`${f.featureCode}: ${f.isEnabled}`)
console.log(" Config:", f.configValues)
}features.checkByExternalOrgId(externalOrgId, featureCode)
Check a specific feature using your external org ID.
const access = await os.features.checkByExternalOrgId("org_abc123", "ai_credit")
if (access.canUse) {
console.log(`Remaining: ${access.remaining}`)
}Billing
Track usage, claim subscriptions, and manage billing.
billing.trackUsage(input)
Track usage for a metered feature.
const result = await os.billing.trackUsage({
companyId: "comp_xxx",
featureCode: "ai_credit",
quantity: 5,
idempotencyKey: `request_${requestId}`, // Prevents double-counting
metadata: { model: "gpt-4", tokens: 1500 },
})
console.log({
currentUsage: result.currentUsage,
remaining: result.remaining,
billable: result.billableQuantity, // Amount beyond included tier
isAtLimit: result.isAtLimit,
})billing.claimCheckout(input)
Claim a subscription from a completed Stripe Checkout Session.
// In your Stripe checkout success handler
const result = await os.billing.claimCheckout({
companyId: "comp_xxx",
checkoutSessionId: "cs_test_xxx", // From Stripe callback
})
if (result.alreadyClaimed) {
console.log("Subscription was already claimed")
} else {
console.log(`Claimed subscription: ${result.subscriptionId}`)
}billing.linkSubscription(input)
Link an existing Stripe subscription to a company.
// When you create subscriptions on YOUR Stripe account
const result = await os.billing.linkSubscription({
companyId: "comp_xxx",
stripeSubscriptionId: "sub_xxx",
stripeCustomerId: "cus_xxx", // Optional: fetched from subscription if omitted
tierCode: "pro", // Apply tier features
displayName: "Pro Plan",
})
console.log({
subscriptionId: result.subscriptionId,
isNew: !result.alreadyLinked,
featuresApplied: result.featuresApplied,
})billing.getStatus(companyId)
Get billing status overview for a company.
const status = await os.billing.getStatus("comp_xxx")
console.log({
hasBilling: status.hasBillingProfile,
subscriptions: status.activeSubscriptions.length,
features: status.enabledFeaturesCount,
})
// Check active tier
if (status.activeSubscriptions[0]?.tier) {
console.log(`Current tier: ${status.activeSubscriptions[0].tier.name}`)
}Subscriptions & Tiers
Manage subscription tiers and apply features.
tiers.list()
List all available tiers for your product.
const tiers = await os.tiers.list()
for (const tier of tiers) {
console.log(`${tier.name} (${tier.code})`)
}
// ['Free', 'Pro', 'Enterprise']tiers.get(tierCode)
Get detailed tier information including Stripe prices.
const tier = await os.tiers.get("pro")
// Check base fee
if (tier.baseFee) {
console.log(`Base fee: ${tier.baseFee.stripe.unitAmount / 100}€/mo`)
console.log(`Price ID: ${tier.baseFee.stripe.priceId}`)
}
// Access features
for (const feature of tier.features) {
console.log(`${feature.code}:`, feature.configValues)
}
// Get all Stripe price IDs (base fee + feature prices)
console.log("Stripe prices:", tier.stripePriceIds)
// ['price_base', 'price_feature1', 'price_feature2']tiers.apply(tierCode, options)
Apply a tier's features to a company (without Stripe).
// Apply free tier
await os.tiers.apply("free", {
companyId: "comp_xxx",
})
// Apply with custom limits
await os.tiers.apply("trial", {
companyId: "comp_xxx",
features: {
ai_credit: { included_quantity: 100 },
seats: { max_quantity: 10 },
},
})subscriptions.create(tierCode, options)
Alias for tiers.apply() — apply a subscription tier.
await os.subscriptions.create("pro", {
companyId: "comp_xxx",
features: {
ai_credit: { included_quantity: 500 },
},
})Links
Manually manage product links.
links.create(input)
Create a manual link between your external org ID and an existing company.
await os.links.create({
companyId: "comp_xxx",
externalOrgId: "org_abc123",
label: "Store Vienna", // Optional: display label
externalUserId: "user_xyz", // Optional: user who created the link
})links.reset(options?)
Delete all product links for your product. Use before re-importing.
// Basic reset (links only)
const result = await os.links.reset()
console.log(`Deleted ${result.linksDeleted} links`)
// Full reset (links + contacts + MRR)
const result = await os.links.reset({
deleteContacts: true,
resetMrr: true,
})
console.log(
`Deleted ${result.linksDeleted} links, ${result.contactsDeleted} contacts`
)Onboarding
Complete customer onboarding in a single API call.
The onboard() method handles:
- Smart company matching/creation
- Product linking
- Subscription claiming/linking
- Tier feature application
Basic Usage
const result = await os.onboard({
externalOrgId: "org_abc123", // Your product's org ID (required)
label: "My Product", // Display label for the product link (required)
hints: {
companyName: "Acme Inc",
domain: "acme.com",
emails: ["[email protected]"],
},
tierCode: "free",
contactEmail: "[email protected]",
contactName: "John Doe",
createdAt: "2024-01-15T10:30:00.000Z", // Optional: historical date
})
console.log({
companyId: result.companyId,
companyName: result.companyName,
linkStatus: result.linkStatus, // 'created' | 'linked' | 'already_linked'
tierApplied: result.billingStatus?.tierApplied,
})Scenario 1: User Completed Stripe Checkout
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
hints: { companyName: "Acme Inc", domain: "acme.com" },
checkoutSessionId: "cs_test_xxx", // From Stripe checkout callback
})
// result.billingStatus.claimed = true
// result.billingStatus.subscriptionId = 'sub_xxx'Scenario 2: Free Tier Signup
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
hints: { companyName: "Acme Inc" },
tierCode: "free",
contactEmail: "[email protected]",
})
// result.billingStatus.tierApplied = trueScenario 3: You Create Stripe Subscription
// 1. Get tier prices
const tier = await os.tiers.get("pro")
// 2. Create subscription on YOUR Stripe account
const stripeSub = await stripe.subscriptions.create({
customer: stripeCustomerId,
items: tier.stripePriceIds.map((price) => ({ price })),
})
// 3. Onboard with the subscription
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
hints: { companyName: "Acme Inc" },
stripeSubscriptionId: stripeSub.id,
stripeCustomerId: stripeCustomerId,
tierCode: "pro",
})
// result.billingStatus.linked = true
// result.billingStatus.tierApplied = trueHandling Existing Customers
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
hints: { companyName: "Acme Inc" },
})
switch (result.linkStatus) {
case "already_linked":
console.log("Welcome back!")
break
case "linked":
console.log("Found existing company, linked!")
break
case "created":
console.log("New company created!")
break
}Common Patterns
Feature Gating
async function checkFeatureAccess(orgId: string, feature: string) {
const access = await os.features.checkByExternalOrgId(orgId, feature)
if (!access.isEnabled) {
throw new Error(`Feature ${feature} is not available on your plan`)
}
if (!access.canUse) {
throw new Error(`Usage limit reached for ${feature}. Please upgrade.`)
}
return access
}
// Usage
await checkFeatureAccess("org_abc123", "ai_credit")
await performAIOperation()
await os.billing.trackUsage({
companyId: company.id,
featureCode: "ai_credit",
quantity: 1,
})Stripe Subscription Flow
// 1. User selects a plan → Get tier prices
const tier = await os.tiers.get("pro")
// 2. Create Stripe Checkout Session with tier prices
const session = await stripe.checkout.sessions.create({
customer: customerId,
line_items: tier.stripePriceIds.map((price) => ({
price,
quantity: 1,
})),
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/cancel`,
})
// 3. After checkout completes → Claim subscription
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
checkoutSessionId: session.id,
})Backfilling Historical Data
import { ChatarminOS } from "@chatarmin/os"
import historicalData from "./migration-data.json"
const os = new ChatarminOS({ apiKey: process.env.OS_API_KEY! })
for (const org of historicalData) {
// Create company with historical date
const result = await os.onboard({
externalOrgId: org.id,
label: "Imported", // Display label for historical import
hints: {
companyName: org.name,
domain: org.domain,
},
tierCode: org.plan,
contactEmail: org.adminEmail,
createdAt: org.createdAt, // Historical timestamp
})
// Sync contacts
await os.contacts.bulkUpsert({
companyId: result.companyId,
contacts: org.users.map((u) => ({
email: u.email,
name: u.name,
role: u.role,
})),
})
}TypeScript Support
The SDK is fully typed with comprehensive TypeScript definitions.
Importing Types
import { ChatarminOS } from "@chatarmin/os"
import type {
ChatarminOSConfig,
CreateCompanyInput,
SmartLinkInput,
SmartLinkResult,
ContactInput,
FeatureCheckInput,
FeatureSetAccessInput,
TrackUsageInput,
OnboardInput,
OnboardResult,
ApplyTierInput,
ClaimCheckoutInput,
LinkSubscriptionInput,
} from "@chatarmin/os"Router Types
import type { AppRouter } from "@chatarmin/os"Configuration
| Option | Type | Required | Default | Description |
| --------- | -------- | -------- | --------------------------------- | --------------------------- |
| apiKey | string | ✅ | — | API key from OS admin panel |
| baseUrl | string | — | https://os.chatarmin.com/api/v1 | API endpoint |
const os = new ChatarminOS({
apiKey: process.env.OS_API_KEY!,
baseUrl: "https://os.chatarmin.com/api/v1", // Default
})
// Local development
const osLocal = new ChatarminOS({
apiKey: process.env.OS_API_KEY!,
baseUrl: "http://localhost:3001/api/v1",
})Getting Your API Key
- Go to ChatarminOS → Settings → Developers → API Keys
- Click "Create API Key"
- Copy the key (format:
os_sk_xxxxxxxxxxxx) - Store in environment variables
Error Handling
The SDK uses tRPC under the hood. Errors include structured information:
try {
await os.billing.trackUsage({
companyId: "comp_xxx",
featureCode: "ai_credit",
quantity: 1000,
})
} catch (error) {
if (error instanceof TRPCClientError) {
console.error("Code:", error.data?.code) // e.g., 'FORBIDDEN'
console.error("Message:", error.message) // Human-readable message
console.error("HTTP Status:", error.data?.httpStatus)
}
}Common Error Codes
| Code | Description |
| --------------------- | -------------------------- |
| UNAUTHORIZED | Invalid or missing API key |
| FORBIDDEN | No access to this resource |
| NOT_FOUND | Company/feature not found |
| BAD_REQUEST | Invalid input parameters |
| PRECONDITION_FAILED | Usage limit exceeded |
Related Documentation
- QUICKSTART.md — Quick publishing & installation guide
- PUBLISHING.md — Detailed publishing instructions
- test-sdk.ts — Complete usage examples
Support
- Email: [email protected]
- Documentation: https://os.chatarmin.com/docs
License
MIT © Chatarmin GmbH
