Type-safe SDK for ChatarminOS - Customer & Subscription Management
npm install @chatarmin/os> Type-safe SDK for ChatarminOS — the all-in-one platform for B2B SaaS to manage customers, subscriptions, feature access, and billing.


- 🏢 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
---
- Installation
- Quick Start
- API Reference
- Companies
- Contacts
- Features
- Billing
- Subscriptions & Tiers
- Links
- Onboarding
- Common Patterns
- TypeScript Support
- Configuration
- Error Handling
---
``bash`
pnpm add @chatarmin/osor
npm install @chatarmin/osor
yarn add @chatarmin/os
---
`typescript
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: "admin@acme.com",
contactName: "John Doe",
})
console.log(result.companyId) // Use this for all future API calls
console.log(result.linkStatus) // 'created' | 'linked' | 'already_linked'
`
---
Manage customer companies and their links to your product.
#### companies.getByProductLink(input)
Look up a company by your external organization ID.
`typescript
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.
`typescript
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: "admin@acme.com",
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).
`typescript`
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.
`typescript
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: ["john@acme.com", "jane@acme.com"],
},
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:
1. Domain match → 95% confidence
2. Name match → up to 80% confidence
3. Email domain match → 70% confidence
---
Manage people associated with companies.
#### contacts.create(input)
Create or update a single contact.
`typescript
const contact = await os.contacts.create({
companyId: "comp_xxx",
email: "john@acme.com",
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.
`typescript
const result = await os.contacts.bulkUpsert({
companyId: "comp_xxx",
contacts: [
{ email: "ceo@acme.com", name: "Alice", role: "CEO", isPrimary: true },
{ email: "cto@acme.com", name: "Bob", role: "CTO" },
{ email: "dev@acme.com", name: "Charlie", role: "Engineer" },
],
})
console.log(Created: ${result.created}, Updated: ${result.updated})`
// result.contacts contains individual results with IDs
#### contacts.list(companyId, primaryOnly?)
List contacts for a company.
`typescript
// 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)
`
---
Check and manage feature access for companies.
#### features.check(input)
Check if a company has access to a specific feature.
`typescript
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.
`typescript
const features = await os.features.listAccess("comp_xxx")
for (const f of features) {
console.log(${f.featureName}: ${f.isEnabled ? "✓" : "✗"}) Usage: ${f.currentUsage} / ${f.quantityLimit}
if (f.quantityLimit) {
console.log()`
}
}
#### features.setAccess(input)
Update feature access with partial config merge.
`typescript
// 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.
`typescript
const result = await os.features.getAccessByExternalOrgId("org_abc123")
console.log(Company: ${result.companyId})${f.featureCode}: ${f.isEnabled}
for (const f of result.features) {
console.log()`
console.log(" Config:", f.configValues)
}
#### features.checkByExternalOrgId(externalOrgId, featureCode)
Check a specific feature using your external org ID.
`typescript
const access = await os.features.checkByExternalOrgId("org_abc123", "ai_credit")
if (access.canUse) {
console.log(Remaining: ${access.remaining})`
}
---
Track usage, claim subscriptions, and manage billing.
#### billing.trackUsage(input)
Track usage for a metered feature.
`typescriptrequest_${requestId}
const result = await os.billing.trackUsage({
companyId: "comp_xxx",
featureCode: "ai_credit",
quantity: 5,
idempotencyKey: , // 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.
`typescript
// 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.
`typescript
// 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.
`typescript
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})`
}
---
Manage subscription tiers and apply features.
#### tiers.list()
List all available tiers for your product.
`typescript
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.
`typescript
const tier = await os.tiers.get("pro")
// Check base fee
if (tier.baseFee) {
console.log(Base fee: ${tier.baseFee.stripe.unitAmount / 100}€/mo)Price ID: ${tier.baseFee.stripe.priceId}
console.log()
}
// 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).
`typescript
// 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.
`typescript`
await os.subscriptions.create("pro", {
companyId: "comp_xxx",
features: {
ai_credit: { included_quantity: 500 },
},
})
---
Manually manage product links.
#### links.create(input)
Create a manual link between your external org ID and an existing company.
`typescript`
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.
`typescriptDeleted ${result.linksDeleted} links
// Basic reset (links only)
const result = await os.links.reset()
console.log()
// Full reset (links + contacts + MRR)
const result = await os.links.reset({
deleteContacts: true,
resetMrr: true,
})
console.log(
Deleted ${result.linksDeleted} links, ${result.contactsDeleted} contacts`
)
---
Complete customer onboarding in a single API call.
The onboard() method handles:
1. Smart company matching/creation
2. Product linking
3. Subscription claiming/linking
4. Tier feature application
#### Basic Usage
`typescript
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: ["john@acme.com"],
},
tierCode: "free",
contactEmail: "admin@acme.com",
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
`typescript
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
`typescript
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
hints: { companyName: "Acme Inc" },
tierCode: "free",
contactEmail: "admin@acme.com",
})
// result.billingStatus.tierApplied = true
`
#### Scenario 3: You Create Stripe Subscription
`typescript
// 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 = true
`
#### Handling Existing Customers
`typescript
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
}
`
---
`typescript
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,
})
`
`typescript
// 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},${baseUrl}/cancel
cancel_url: ,
})
// 3. After checkout completes → Claim subscription
const result = await os.onboard({
externalOrgId: "org_abc123",
label: "My Product",
checkoutSessionId: session.id,
})
`
`typescript
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,
})),
})
}
`
---
The SDK is fully typed with comprehensive TypeScript definitions.
`typescript`
import { ChatarminOS } from "@chatarmin/os"
import type {
ChatarminOSConfig,
CreateCompanyInput,
SmartLinkInput,
SmartLinkResult,
ContactInput,
FeatureCheckInput,
FeatureSetAccessInput,
TrackUsageInput,
OnboardInput,
OnboardResult,
ApplyTierInput,
ClaimCheckoutInput,
LinkSubscriptionInput,
} from "@chatarmin/os"
`typescript`
import type { AppRouter } from "@chatarmin/os"
---
| Option | Type | Required | Default | Description |
| --------- | -------- | -------- | --------------------------------- | --------------------------- |
| apiKey | string | ✅ | — | API key from OS admin panel |baseUrl
| | string | — | https://os.chatarmin.com/api/v1 | API endpoint |
`typescript
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",
})
`
1. Go to ChatarminOS → Settings → Developers → API Keys
2. Click "Create API Key"
3. Copy the key (format: os_sk_xxxxxxxxxxxx)
4. Store in environment variables
---
The SDK uses tRPC under the hood. Errors include structured information:
`typescript`
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)
}
}
| 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 |
|
---
- QUICKSTART.md — Quick publishing & installation guide
- PUBLISHING.md — Detailed publishing instructions
- test-sdk.ts — Complete usage examples
---
- Email: hello@chatarmin.com
- Documentation: https://os.chatarmin.com/docs
---
MIT © Chatarmin GmbH