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/jsIf 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_*) andteamId. - 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)
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 objectCustomer 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-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.idError 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 matchesAdmin 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
});