npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

npm version TypeScript

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/os

Quick 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 IDs

contacts.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 every os.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 slug
  • data[].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,projectedOverageCents

billing.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 url

billing.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:

  1. Smart company matching/creation
  2. Product linking
  3. Subscription claiming/linking
  4. 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 = true

Scenario 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 = true

Handling 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

  1. Go to ChatarminOS → SettingsDevelopersAPI Keys
  2. Click "Create API Key"
  3. Copy the key (format: os_sk_xxxxxxxxxxxx)
  4. 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


Support


License

MIT © Chatarmin GmbH