@chatarmin/os
v1.8.9
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' | 'already_linked'API Reference
Companies
Manage customer companies and their links to your product.
companies.get(input)
Look up a company by your external organization ID or ChatarminOS company ID.
const company = await os.companies.get({
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",
})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.
Identifiers: pass featureCode on input (e.g. "ai_credit"). Responses include featureCode and featureAccessId. Codes are set at feature create and are not renamed via the API — change display with featureName, or create a new feature and migrate for a real rename. See product-billing-api.md for details.
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.
API reference:
docs/engineering/product-billing-api.md— full request/response shapes for everyos.billing.*method.
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(companyIdOrInput)
Quick Stripe subscription status for a company — activeSubscriptions (active / trialing / past_due), resolved billing profile, and enabled feature count. Scoped to the product link's billing profile when externalOrgId is provided.
See Product-scoped billing profile — getStatus example for the full walkthrough + sample response.
const status = await os.billing.getStatus('ee0f4a19-2b69-4969-8807-d0013f1670ee')
const statusByOrg = await os.billing.getStatus({ externalOrgId: 'org_kKZHiUHBSjdu' })
if (status.activeSubscriptions[0]?.status === 'past_due') {
// gate access for this external org
}
console.log({
stripeCustomer: status.billingProfile?.stripeCustomerId,
subscriptionStatus: status.activeSubscriptions[0]?.status,
features: status.enabledFeaturesCount,
})billing.getOverview(input)
Combined billing page payload: company, billing profile, primary subscription, and recent invoices.
const overview = await os.billing.getOverview({
companyId: "comp_xxx",
invoiceLimit: 12,
includeUsage: true, // optional: embed usage rows
})
console.log({
plan: overview.subscription?.tier?.name,
status: overview.subscription?.status,
paymentMethod: overview.billingProfile?.primaryPaymentMethod,
invoices: overview.invoices.length,
usageRows: overview.usage?.length,
})billing.getUsage(companyIdOrInput, options?)
Metronome-style usage aggregates. Returns { data: UsageRecord[] } — feature-first nesting with per-feature billing cycles.
Company scope: pass companyId as the first string argument, or pass an input object with companyId or externalOrgId (one required). Prefer externalOrgId when CX only knows the org slug — usage is scoped to that product link.
// CX-native (single product link)
const { data } = await os.billing.getUsage({
externalOrgId: 'org_FVCNOtEdpeeP',
windowSize: 'none',
})
// Or companyId as first arg
const byCompany = await os.billing.getUsage('comp_xxx', { windowSize: 'none' })data[].featureCode— stable feature slugdata[].cycles[]— billing cycles for this feature (periodOffset, bounds,usage[])
windowSize controls whether usage points are period totals or time buckets. Filter with featureCode (stable slug), not featureAccessId — see Feature identifiers.
periodOffsetRange (time scope for whole billing cycles):
| Intent | periodOffsetRange |
|--------|---------------------|
| Current cycle (default) | omit or { from: 0, to: 0 } |
| Previous only | { from: -1, to: -1 } |
| Current + previous | { from: -1, to: 0 } |
| 12-cycle picker | { from: -11, to: 0 } |
Multi-cycle range requires windowSize: 'none' (max 12 cycles). Cannot combine with startingOn/endingBefore.
Every cycles[] bucket includes subscriptionItem and summary when it has usage rows. usage[] stays thin (value + dates).
Response shape:
interface UsageQueryResult {
data: UsageRecord[]
}
interface UsageRecord {
featureCode: string
featureName: string
cycles: UsageCycleBucket[]
}
interface UsageCycleBucket {
periodOffset: number
startingOn: Date
endingBefore: Date
label: string
subscriptionItem?: UsageSubscriptionItem // present when cycle has usage rows
summary?: UsageCycleSummary // totals + projection; present when cycle has usage rows
usage: UsagePoint[] // thin: value + dates (+ optional identity/grouping)
}
interface UsagePoint {
value: number
startingOn: Date
endingBefore: Date
featureAccessId?: string
meterCode?: string
groupKey?: string | null // with groupBy: 'productLink'
groupValue?: string | null
productLinkId?: string | null // groupBy: 'productLink', or bucketed multi-link companyId
productLinkLabel?: string | null
externalOrgId?: string | null
}Read billing from cycle.summary and cycle.subscriptionItem — not from usage[] points.
productLinkId (+ label, externalOrgId) appears on usage[] points when groupBy: 'productLink', or on bucketed companyId queries when multiple product links share the company (without groupKey).
| windowSize | cycles[].usage | Billing context |
| ------------ | ---------------- | --------------- |
| 'none' | 1 point | subscriptionItem + summary (always) |
| 'day' / 'week' / 'month' | N thin points | Same — billing once per cycle, not per bucket |
Examples by windowSize
windowSize: 'none' — period totals:
{
"data": [{
"featureCode": "messages",
"featureName": "Messages",
"cycles": [{
"periodOffset": 0,
"startingOn": "2026-07-01T00:00:00.000Z",
"endingBefore": "2026-08-01T00:00:00.000Z",
"label": "Jul 1, 2026 – Aug 1, 2026",
"subscriptionItem": {
"includedQuantity": 100,
"unitAmountCents": 500,
"currency": "eur"
},
"summary": {
"totalUsage": 120,
"billableQuantity": 20,
"estimatedOverageCents": 10000,
"projectedBillableQuantity": 240,
"projectedOverageCents": 70000
},
"usage": [{
"value": 120,
"startingOn": "2026-07-01T00:00:00.000Z",
"endingBefore": "2026-08-01T00:00:00.000Z"
}]
}]
}]
}windowSize: 'day' — daily chart buckets:
{
"data": [{
"featureCode": "messages",
"featureName": "Messages",
"cycles": [{
"periodOffset": 0,
"subscriptionItem": { "includedQuantity": 100 },
"summary": { "totalUsage": 20, "billableQuantity": 0 },
"usage": [
{ "value": 8, "startingOn": "2026-07-01T00:00:00.000Z", "endingBefore": "2026-07-02T00:00:00.000Z" },
{ "value": 12, "startingOn": "2026-07-02T00:00:00.000Z", "endingBefore": "2026-07-03T00:00:00.000Z" }
]
}]
}]
}Multi-cycle (periodOffsetRange: { from: -1, to: 0 }):
{
"data": [{
"featureCode": "messages",
"featureName": "Messages",
"cycles": [
{ "periodOffset": 0, "label": "Jul 1 – Aug 1", "summary": { "totalUsage": 120, "billableQuantity": 20 }, "usage": [{ "value": 120 }] },
{ "periodOffset": -1, "label": "Jun 1 – Jul 1", "summary": { "totalUsage": 98, "billableQuantity": 0 }, "usage": [{ "value": 98 }] }
]
}]
}Full examples: docs/engineering/product-billing-api.md#billinggetusage
const { data } = await os.billing.getUsage('comp_xxx', { windowSize: 'none' })
const messages = data.find((r) => r.featureCode === 'messages')
const cycle = messages?.cycles[0]
const currentTotal = cycle?.usage[0]?.value
const overage = cycle?.summary?.billableQuantity
const cost = cycle?.summary?.estimatedOverageCents
const daily = await os.billing.getUsage('comp_xxx', {
windowSize: 'day',
featureCode: 'messages',
})
const dailySeries = (daily.data[0]?.cycles[0]?.usage ?? []).map((point) => ({
day: point.startingOn.toISOString().slice(0, 10),
usage: point.value,
}))
// Previous billing period totals
const lastMonth = await os.billing.getUsage('comp_xxx', {
windowSize: 'none',
periodOffsetRange: { from: -1, to: -1 },
})
// Last 12 billing cycles (cycle picker)
const history = await os.billing.getUsage('comp_xxx', {
windowSize: 'none',
periodOffsetRange: { from: -11, to: 0 },
})
// history.data[0].cycles → one bucket per period; cycle.usage[0] → period total
// Custom sub-range within the current billing period (both dates required)
// usage[] points cover the sub-range; summary totals match it; projections use the full cycle
const week = await os.billing.getUsage('comp_xxx', {
windowSize: 'day',
featureCode: 'messages',
startingOn: new Date('2026-07-10T00:00:00Z'),
endingBefore: new Date('2026-07-17T00:00:00Z'),
})| windowSize | Shape |
| ------------ | ----- |
| 'none' | One point per feature (collapsed total when multiple product links share a company; use groupBy: 'productLink' for per-link rows) |
| 'day' / 'week' / 'month' | One point per feature per bucket; one per link per bucket when multiple product links share the company (productLinkId on each point) |
| Time scope | When to use |
| ---------- | ----------- |
| omit | Current billing period (default) |
| periodOffsetRange: { from: -1, to: -1 } | Whole previous billing period |
| periodOffsetRange: { from: -11, to: 0 } | Last 12 billing cycles |
| startingOn + endingBefore | Sub-range inside the current period (clamped; totals follow sub-range; projections use full cycle) |
billing.exportUsage(companyIdOrInput, options?)
Export usage aggregates as CSV. Accepts the same company scope and query options as getUsage (plus implicit format: 'csv'). CSV includes a periodOffset column (first column) for multi-cycle exports. Filename uses exported period bounds: usage_{windowSize}_{companyId}_{periodStart}_{periodEnd}.csv.
const csv = await os.billing.exportUsage({
externalOrgId: 'org_FVCNOtEdpeeP',
windowSize: 'none',
periodOffsetRange: { from: -11, to: 0 },
})
// csv.filename → "usage_none_<companyId>_2026-06-01_2026-08-01.csv"
// csv.content → first line:
// periodOffset,featureCode,featureName,value,startingOn,endingBefore,groupKey,groupValue,productLinkId,productLinkLabel,externalOrgId,includedQuantity,billableQuantity,quantityLimit,remaining,percentUsed,unitAmountCents,currency,estimatedOverageCents,projectedBillableQuantity,projectedOverageCentsbilling.listInvoices(companyIdOrInput, options?)
List invoices for a company (excludes draft/void).
const { invoices } = await os.billing.listInvoices("comp_xxx", { limit: 24 })billing.getPaymentMethod(companyIdOrInput)
Payment method summary for the resolved billing profile.
const { billingProfile } = await os.billing.getPaymentMethod("comp_xxx")billing.createPortalSession(input)
Open Stripe Customer Portal for manage-billing flows. returnUrl must be on an allowed product host (products.metadata.billing.returnUrlHosts).
const { url } = await os.billing.createPortalSession({
companyId: "comp_xxx",
returnUrl: "https://app.example.com/settings/billing",
})
// When a company has multiple Stripe customers, target a specific billing profile:
const { url: profileUrl } = await os.billing.createPortalSession({
companyId: "comp_xxx",
stripeCustomerId: "cus_xxx",
returnUrl: "https://app.example.com/settings/billing",
})
// Redirect the user to urlbilling.resolveInvoicePaymentUrl(input)
Resolve a hosted invoice payment URL for a specific invoice.
const { hostedInvoiceUrl } = await os.billing.resolveInvoicePaymentUrl({
companyId: "comp_xxx",
invoiceId: "inv_xxx",
})billing.listPastDuePaymentUrls(companyIdOrInput)
List overdue invoices with payment URLs.
const { items } = await os.billing.listPastDuePaymentUrls("comp_xxx")billing.sendPastDueInvoice(input)
Email a past-due invoice via Stripe (when collection method is send_invoice).
await os.billing.sendPastDueInvoice({
companyId: "comp_xxx",
invoiceId: "inv_xxx",
})billing.syncCompanyFromStripe(input)
Pull billing profile, subscriptions, and feature links from Stripe.
const result = await os.billing.syncCompanyFromStripe({
companyId: "comp_xxx",
stripeCustomerId: "cus_xxx",
})
console.log({
subscriptionsSynced: result.subscriptionsSynced,
featuresLinked: result.featuresLinked,
})billing.relinkBillingProfile(input)
Replace the company's billing profile with a new Stripe customer (destructive).
await os.billing.relinkBillingProfile({
companyId: "comp_xxx",
stripeCustomerId: "cus_new_xxx",
})billing.ensureFeatureSubscription(input)
Ensure a metered feature is linked to a Stripe subscription item.
const result = await os.billing.ensureFeatureSubscription({
companyId: "comp_xxx",
featureCode: "ai_credit",
})
console.log(result.action) // 'already_linked' | 'linked_existing' | 'created_new' | 'failed'billing.setUsage(input)
Set absolute usage (corrections / admin). Requires a reason for audit.
const result = await os.billing.setUsage({
companyId: "comp_xxx",
featureCode: "messages",
absoluteValue: 420,
reason: "Reconcile after import",
})
console.log({
previous: result.previousUsage,
current: result.currentUsage,
delta: result.delta,
})billing.getUsageHistory(companyId, featureCode, options?)
Raw usage events for audit/debug (not aggregated rollups).
const events = await os.billing.getUsageHistory("comp_xxx", "messages", {
limit: 50,
eventType: "increment",
})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' | '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 "created":
console.log("New company created!")
break
}Product Webhooks
When ChatarminOS pushes company and product-link changes to your app, verify the request with the SDK before handling the payload.
import { webhooks } from "@chatarmin/os/webhooks"
export async function POST(request: Request) {
const rawBody = await request.text()
const event = webhooks.constructEvent(
rawBody,
request.headers,
process.env.OS_WEBHOOK_SIGNING_SECRET!,
)
switch (`${event.type}.${event.action}`) {
case "company.updated":
// event.data, event.webhookId, event.idempotency_key
break
}
return Response.json({ received: true })
}Use ChatarminOS.webhooks.constructEvent if you already import the main client. Dedupe on event.webhookId or event.idempotency_key.
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,
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:3000/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
- Product billing API — Billing read APIs,
externalOrgIdscoping, and preview test tenants - 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
