Skip to main content

JavaScript SDK

@cstar.help/js is the core client library. It works in Node.js, Deno, Bun, and browsers — anywhere with fetch. React and Svelte SDKs build on top of this.

Installation

npm install @cstar.help/js

If you're using @cstar.help/react or @cstar.help/svelte, you already have this — it's included automatically as a dependency.

Three Client Types

The SDK provides three clients for different use cases:

  • CStarClient — Server-side admin API. Full CRUD for tickets, customers, articles, webhooks. Requires a secret API key (sk_live_*) and teamId.
  • ChatClient — Customer-facing chat widget. Real-time messaging with HMAC identity verification. Uses teamSlug.
  • LibraryClient — Public knowledge base. No auth required. Browse categories, articles, and search. Uses teamSlug.

CStarClient (Admin API)

client.js
import { CStarClient } from '@cstar.help/js';

const cstar = new CStarClient({
  apiKey: 'sk_live_your_key',
  teamId: 'your-team-id', // Required — see /whoami endpoint below
  // Optional: override base URL (defaults to https://www.cstar.help)
  // baseUrl: 'https://custom-domain.com'
});

Finding Your Team ID

Use the /whoami endpoint to discover your teamId from your API key.

curl -H "Authorization: Bearer sk_live_your_key" \
  https://www.cstar.help/api/v1/auth/whoami
{
  "success": true,
  "data": {
    "teamId": "your-team-uuid",
    "teamName": "Acme Corp",
    "teamSlug": "acme",
    "keyType": "secret",
    "environment": "live"
  }
}

Or use the CLI: cstar status (after cstar login).

TypeScript Types

The SDK is fully typed. Click to expand any type definition.

Tickets

// List tickets with filters
const { data, pagination } = await cstar.tickets.list({
  status: 'open',
  priority: 'high',
  page: 1,
  pageSize: 20
});

// Get a single ticket (includes messages)
const ticket = await cstar.tickets.get('tkt_abc123');

// Create a ticket
const newTicket = await cstar.tickets.create({
  title: 'Bug report: login page',
  priority: 'high',
  customerName: 'Jane Smith'
});

// Update a ticket
await cstar.tickets.update('tkt_abc123', {
  status: 'resolved'
});

// Metadata — merged on update (existing keys preserved)
await cstar.tickets.create({
  title: 'Bug report',
  metadata: { source: 'sdk', version: '1.0' }
});
await cstar.tickets.update('tkt_abc123', {
  metadata: { priority_override: 'p0' }
  // Result: { source: 'sdk', version: '1.0', priority_override: 'p0' }
  // Set a key to null to remove it
});

// Delete a ticket — note: method is .del(), not .delete()
await cstar.tickets.del('tkt_abc123');

Customers

// List customers
const { data } = await cstar.customers.list({
  status: 'active',
  search: 'jane'
});

// Get customer details
const customer = await cstar.customers.get('cus_abc123');

// Create a customer
const newCustomer = await cstar.customers.create({
  name: 'Jane Smith',
  email: 'jane@example.com',
  tags: ['vip', 'enterprise']
});

// Update a customer
await cstar.customers.update('cus_abc123', {
  sentiment: 'positive'
});

Articles

// List published articles
const { data } = await cstar.articles.list({
  status: 'published',
  category: 'Getting Started'
});

// Get an article
const article = await cstar.articles.get('art_abc123');

// Create an article
const draft = await cstar.articles.create({
  title: 'How to reset your password',
  content: '# Password Reset\n\nFollow these steps...',
  category: 'Account',
  status: 'draft'
});

// Publish an article
await cstar.articles.update('art_abc123', {
  status: 'published',
  isPublic: true
});

Note on slugs: When creating an article, the slug you provide is used as a prefix. cStar appends a unique suffix (e.g., reset-password-mn46zcig) to prevent collisions. The final slug is returned in the response.

Categories

// List categories
const { data } = await cstar.categories.list();

// List only public categories
const { data: publicCats } = await cstar.categories.list({ isPublic: true });

// Create a category
const category = await cstar.categories.create({
  name: 'Getting Started',
  description: 'Onboarding and setup guides',
  isPublic: true,
  sortOrder: 0
});
// slug is auto-generated from name: "getting-started"

// Update a category
await cstar.categories.update(category.data.id, {
  description: 'Updated description',
  sortOrder: 1
});

// Delete a category
await cstar.categories.del(category.data.id);

Auto-Pagination

Every list method has a listAutoPaginating() variant that returns an async iterator. It automatically fetches the next page when you exhaust the current one — no manual page tracking needed.

// Iterate through ALL tickets automatically
for await (const ticket of cstar.tickets.listAutoPaginating({ pageSize: 50 })) {
  console.log(ticket.title);
  // Break early if you only need a subset
  if (someCondition) break;
}

// Works on every resource
for await (const customer of cstar.customers.listAutoPaginating({ status: 'active' })) {
  console.log(customer.name);
}

Messages

// List messages for a ticket
const messages = await cstar.tickets.messages.list('tkt_abc123');

// Add a message
await cstar.tickets.messages.create('tkt_abc123', {
  content: 'Thanks for reaching out! Let me look into this.',
  sender: 'agent',
  senderName: 'Bob (Support)'
});

Webhooks

// List webhooks
const webhooks = await cstar.webhooks.list();

// Create a webhook
const webhook = await cstar.webhooks.create({
  name: 'Slack Integration',
  url: 'https://your-app.com/webhook',
  events: ['ticket.created', 'ticket.closed']
});

// Save the secret! Only shown once.
console.log('Webhook secret:', webhook.secret);

// Trigger a test event
await cstar.webhooks.test(webhook.data.id, { event: 'ticket.created' });

Webhook Verification

import { constructEvent } from '@cstar.help/js/webhook';

// In your webhook handler (Express, Next.js, etc.)
const event = constructEvent(
  requestBody,   // Raw request body string
  signature,     // x-cstar-signature header
  webhookSecret  // Your webhook secret
);

console.log(event.type);  // 'ticket.created'
console.log(event.data);  // The ticket object

Customer Authentication

Authenticate end-customers via email and password. Use this for custom widget integrations or headless support experiences.

import { AuthClient } from '@cstar.help/js/auth';

const auth = new AuthClient({ teamSlug: 'acme' });

// Sign up a new customer
const result = await auth.signup({
  email: 'jane@example.com',
  password: 'securepass123',
  name: 'Jane Doe',
});

// If email confirmation is required:
if ('requiresConfirmation' in result) {
  console.log(result.message); // 'Please check your email...'
}

// Log in an existing customer
await auth.login({
  email: 'jane@example.com',
  password: 'securepass123',
});

// Check session state
console.log(auth.isAuthenticated); // true
console.log(auth.accessToken);     // 'eyJ...'
console.log(auth.getCustomer());   // { id, email, name }

// Log out
auth.logout();

Public Knowledge Base

Query your public knowledge base — no authentication required. Use this to build custom help centers or embed articles in your app.

import { LibraryClient } from '@cstar.help/js/library';

const library = new LibraryClient({ teamSlug: 'acme' });

// List categories
const categories = await library.categories();

// List articles (with optional category filter)
const articles = await library.articles({ categorySlug: 'billing', limit: 10 });

// Get a single article by slug
const article = await library.article('reset-password');

// Full-text search
const results = await library.search('reset password', { limit: 5 });

// Get most popular articles by view count
const popular = await library.popularArticles(5);

// Get aggregate stats
const stats = await library.stats();
// { totalArticles: 42, totalCategories: 6, totalViews: 1200 }

// Record a view for analytics
await library.recordView('reset-password');

// Submit article feedback ("Was this helpful?")
await library.submitFeedback('reset-password', {
  isHelpful: true,
  feedbackText: 'Very clear instructions!',
});

Chat Client

Customer-facing real-time chat. Supports Supabase Realtime for instant message delivery with automatic polling fallback. Requires HMAC identity verification for security.

import { ChatClient } from '@cstar.help/js/chat';

const chat = new ChatClient({
  teamSlug: 'acme',
  supabaseUrl: 'https://your-project.supabase.co',
  supabaseAnonKey: 'your-anon-key',
  // pollingInterval: 3000  // Fallback interval (default)
});

// Identify the customer (HMAC signature from your server)
const result = await chat.identify({
  externalId: 'user_123',
  email: 'jane@example.com',
  name: 'Jane Doe',
  timestamp: Math.floor(Date.now() / 1000)
}, 'hmac_signature');

// List conversations
const convos = await chat.conversations.list();

// Create a conversation
const convo = await chat.conversations.create({
  subject: 'Need help',
  message: 'Hi, I have a question.'
});

// Send a message
const msg = await chat.messages.send(convo.id, 'Thanks!');

// Listen for new messages on a conversation
const unsubMessages = chat.onMessage(convo.id, (message) => {
  console.log('New message:', message.content);
});

// Listen for typing indicators on a conversation
const unsubTyping = chat.onTyping(convo.id, (payload) => {
  console.log(payload.agent_name, 'is typing');
});

// Unsubscribe when done
// unsubMessages();
// unsubTyping();

// Clean up
chat.disconnect();

Computing the HMAC Signature

The signature must be computed server-side (never expose your HMAC secret to the browser). Sign a sorted JSON payload of the customer fields with HMAC-SHA256, and pass the hex output to identify().

server.js
// Server-side: compute the HMAC signature
import crypto from 'crypto';

function computeSignature(customer, hmacSecret) {
  // 1. Build payload with only these fields (sorted alphabetically)
  const signed = {
    email: customer.email,
    externalId: customer.externalId,
    name: customer.name,         // include if provided
    timestamp: customer.timestamp // Unix seconds — required
  };

  // 2. Remove undefined fields, sort keys, compact JSON
  const filtered = Object.fromEntries(
    Object.entries(signed)
      .filter(([, v]) => v !== undefined)
      .sort(([a], [b]) => a.localeCompare(b))
  );
  const payload = JSON.stringify(filtered);
  // Example: {"email":"jane@example.com","externalId":"user_123","name":"Jane Doe","timestamp":1700000000}

  // 3. HMAC-SHA256 → hex (no prefix, just the hex string)
  return crypto.createHmac('sha256', hmacSecret).update(payload).digest('hex');
}

// Usage in your API route:
const signature = computeSignature({
  externalId: 'user_123',
  email: 'jane@example.com',
  name: 'Jane Doe',
  timestamp: Math.floor(Date.now() / 1000)
}, process.env.CSTAR_HMAC_SECRET);
// Send this signature to the browser for chat.identify()

Timestamp replay protection: Signatures expire after 5 minutes in production and 1 hour in test mode. The HMAC secret is found in your dashboard under Settings → Widget → Identity Verification.

Idempotency

Pass an idempotencyKey as a request option to safely retry create and update calls. If the same key is sent again within 24 hours, the original response is returned — no duplicate resource created.

// Safe to retry — same key always returns the same ticket
const ticket = await cstar.tickets.create(
  { title: 'Order issue' },
  { idempotencyKey: 'order-issue-abc123' }
);

// Network error? Retry with the same key:
const retry = await cstar.tickets.create(
  { title: 'Order issue' },
  { idempotencyKey: 'order-issue-abc123' }
);
// retry.data.id === ticket.data.id

Error Handling

All API errors throw typed error classes. Rate limit errors include a retryAfter value in seconds.

import { CStarError, CStarRateLimitError } from '@cstar.help/js';

try {
  await cstar.tickets.create({ /* missing title */ });
} catch (err) {
  if (err instanceof CStarRateLimitError) {
    // 429 — back off and retry
    console.log(err.retryAfter); // seconds to wait (e.g. 3600)
    await sleep(err.retryAfter * 1000);
  } else if (err instanceof CStarError) {
    console.log(err.code);       // 'parameter_missing'
    console.log(err.message);    // 'title is required'
    console.log(err.statusCode); // 400
    console.log(err.param);      // 'title'
  }
}

Public Community

Browse and search your public community forum — no authentication required. Import CommunityClient from the @cstar.help/js/community entry point.

Setup

import { CommunityClient } from '@cstar.help/js/community';

const community = new CommunityClient({ teamSlug: 'acme' });

Topics

// List all discussion topics
const topics = await community.topics();

Posts

// List posts with filters
const { posts, count } = await community.posts({
  topicSlug: 'feature-requests',
  sort: 'votes',    // 'recent' | 'votes' | 'comments'
  limit: 20,
  offset: 0
});

// Get a single post by slug (includes comments)
const { post, comments } = await community.post('dark-mode-support');

Search

// Full-text search across post titles and bodies
const results = await community.search('billing');
// results.data — matching posts
// results.count — total matches

Admin Operations (API Key Required)

For server-side management (create, update, delete, vote, follow), use CStarClient.community with your secret API key.

import { CStarClient } from '@cstar.help/js';

const cstar = new CStarClient({ apiKey: 'sk_live_...', teamId: 'tm_...' });

// Create a post (with optional custom fields scoped to the topic)
await cstar.community.posts.create({
  title: 'Tutorial Video',
  topicId: 'topic_abc',
  body: 'Check out this walkthrough',
  customFields: { 'video_url_field_id': 'https://youtube.com/watch?v=abc' }
});

// Vote, follow, comment
await cstar.community.posts.vote('post_id');
await cstar.community.posts.follow('post_id');
await cstar.community.posts.createComment('post_id', {
  body: 'Great idea!',
  isOfficial: true
});