@cstar.help/js
Official TypeScript SDK for the cStar customer support platform.
- Native fetch for HTTP, Supabase for realtime
- TypeScript-first, with full type definitions included
- Focused entry points for the part of cStar you're building with
- ESM + CJS exports
Install
npm install @cstar.help/js
Which SDK Do I Need?
cStar has one package with separate entry points for different workflows. Start here:
| Import | Use Case | Runs On | Auth Required | Key Type |
|---|---|---|---|---|
@cstar.help/js | Admin dashboard / backend integration | Server only | Yes | Secret (sk_*) |
@cstar.help/js/chat | Customer chat widget | Browser | Yes | HMAC or customer session |
@cstar.help/js/library | Public knowledge base | Browser or server | No | None |
@cstar.help/js/community | Public community forum | Browser or server | No | None |
@cstar.help/js/webhook | Webhook signature verification | Server only | No | Signing secret |
@cstar.help/js/auth | Customer signup and login | Browser | No | None (teamSlug) |
The Security Model
The server API and customer chat authenticate differently:
- Secret API keys (
sk_live_*,sk_test_*) authorize team API operations within the key's permissions. Keep them on your server. - Publishable API keys (
pk_live_*,pk_test_*) are for the public SDKs (LibraryClient,CommunityClient) andGET /auth/whoami. Team API endpoints refuse them with 403:publishable_key_forbiddenon reads andpublishable_key_writeon writes, soCStarClientneeds a secret key. They do not identify a customer or authorize customer chat. - ChatClient takes a
teamSlug, then authenticates a customer through HMAC identity verification or a customer session. It has no API-key option. Keep HMAC signing secrets on your server too.
Building a support dashboard? Use
CStarClienton your backend. Adding a conversation to your website? UseChatClientand one of the customer authentication options below.
Server-Side: Admin API Client
⚠️ Server-side only.
CStarClientrequires a secret key (sk_*) and must never be used in browser code. The SDK will warn you if it detects a browser environment.
Manage tickets, customers, articles, team members, settings, game system, and everything else.
import { CStarClient } from '@cstar.help/js';
const apiKey = process.env.CSTAR_SECRET_KEY; // sk_live_... or sk_test_...
const teamId = process.env.CSTAR_TEAM_ID;
if (!apiKey || !teamId) throw new Error('Set CSTAR_SECRET_KEY and CSTAR_TEAM_ID.');
const cstar = new CStarClient({ apiKey, teamId });
// List open tickets
const { data: tickets } = await cstar.tickets.list({ status: 'open' });
// Create a ticket
const ticket = await cstar.tickets.create({
title: 'Help with billing',
customerId: 'cus_abc123'
});
// Auto-paginate through all tickets
for await (const ticket of cstar.tickets.listAutoPaginating({ status: 'open' })) {
console.log(ticket.title);
}
Created, retrieved and updated resources are returned directly. List methods return { data, pagination, hasMore }. Search returns { results, total, query }. Read cstar.lastMeta.requestId immediately after a call when you need its request ID.
Private notes and message history
Run message management on your server with a secret key. Internal notes stay inside the support team.
const note = await cstar.tickets.messages.create('tkt_123', {
content: 'The customer has already tried resetting their password.',
messageType: 'internal'
});
console.log(note.isInternal);
const page = await cstar.tickets.messages.list('tkt_123', { page: 2, pageSize: 25 });
console.log(page.data, page.pagination.hasMore);
The isInternal boolean is also accepted when creating a note. If you supply both privacy options, they must agree. Message history is paginated, so request the next page while hasMore is true.
Full Resource Reference
Core Resources: Tickets, customers, and knowledge base.
| Resource | Methods |
|---|---|
cstar.tickets | .list() .get(id) .create(params) .update(id, params) .del(id) .listAutoPaginating() |
cstar.tickets.messages | .list(ticketId, params?) .create(ticketId, params) |
cstar.customers | .list() .get(id) .create(params) .update(id, params) .del(id) .listAutoPaginating() |
cstar.articles | .list() .get(id) .create(params) .update(id, params) .del(id) .listAutoPaginating() |
cstar.categories | .list() .get(id) .create(params) .update(id, params) .del(id) |
Team & Configuration: Members, settings, and customization.
| Resource | Methods |
|---|---|
cstar.members | .list() .get(id) .update(id, params) .del(id) |
cstar.members.invites | .list() .create(params) .del(id) |
cstar.settings | .get() .update(params) .getBusinessHours() .updateBusinessHours(params) .getWidget() .updateWidget(params) |
cstar.settings.statuses | .list() .create(params) .update(key, params) .del(key) .reorder(keys) |
cstar.settings.quickReplies | .list() .create(params) .update(id, params) .del(id) .reorder(ids) |
cstar.customFields | .list() .create(params) .update(id, params) .del(id) .reorder(ids) |
cstar.notifications | .list({ userId }) .unreadCount(userId) .markRead(id, userId) .markAllRead(userId) |
Operations: Analytics, bulk actions, and data management.
| Resource | Methods |
|---|---|
cstar.analytics | .overview(params?) .performance(params?) .library(params?) .csatStats(params?) |
cstar.sla | .list() .create(params) .update(id, params) .del(id) .reorder(ids) .metrics(params?) |
cstar.bulk | .update(params) .del(params) |
cstar.export | .create(params) |
cstar.import | .create(params) .logs(params?) .getLog(id) |
cstar.search | .query(params) |
cstar.tags | .list() .merge(params) .remove(tag) .del(tag) |
cstar.activity | .list(params?) |
cstar.auditLog | .list(params?) |
cstar.customerGroups | .list() .get(id) .create(params) .update(id, params) .del(id) |
Gamification: The full game system.
| Resource | Methods |
|---|---|
cstar.game | .player(params?) .getPlayer(memberId) .leaderboard(params?) .myRank(params?) |
cstar.game.boss | .current() .isActive() .spawn(params?) .damage(params) .defeats(params?) .getDefeat(id) |
cstar.game.achievements | .list(params?) .unlocked(params) .get(id, params?) |
cstar.game.quests | .today(params?) .history(params?) |
cstar.game.puzzles | .today(params?) .complete(params) .streak(params) .leaderboard(params) |
cstar.game.oneUps | .list(params?) .send(params) .remaining(params) |
cstar.game.highFives | .list(params?) .send(params) .canSend(teamId, params) |
cstar.game.cosmetics | .list(params) .getPreferences(params) .updatePreferences(params) |
cstar.game.seasons | .list(params?) .current() .stats(seasonId, params) |
cstar.game.skills | .get(params) .allocate(params) .reset(params) |
Game writes belong on your server with a secret API key. Use user and team UUIDs from cStar. One-ups and high fives default their sender to the key creator. Puzzle scoreboards require a puzzleId; streak boards use { type: 'streaks' }. Cosmetic lists require a userId.
Cosmetic and season lists return { data, pagination, hasMore }. One-up and high-five lists return { object: 'list', data }. Today's quests return { quests, completedToday, totalQuests, allComplete, resetsAt } (each quest pays its own xpReward and goldReward; there is no all-quests bonus). Single resources return bare objects.
Advanced: AI, automations, views, billing, webhooks.
| Resource | Methods |
|---|---|
cstar.ai | .run(params) .autoTag(ticketId) .suggestReply(ticketId) .summarize(ticketId) .improve(text) |
cstar.automations | .list() .get(id) .create(params) .update(id, params) .del(id) .toggle(id) .reorder(ids) .test(id, params) .executions(id) .allExecutions() .evaluate(params) |
cstar.views | .list() .get(id) .create(params) .update(id, params) .del(id) .execute(id, params?) |
cstar.billing | .subscription() .portal(params?) |
cstar.webhooks | .list() .get(id) .create(params) .update(id, params) .del(id) .test(id) |
cstar.community.posts | .list() .get(id) .create(params) .update(id, params) .del(id) |
cstar.community.topics | .list() .get(id) |
cstar.attachments | .upload(file, params) .list(params?) .get(id) .del(id) |
Usage Examples
// Team analytics
const overview = await cstar.analytics.overview({ period: 'week' });
console.log(`${overview.tickets?.open ?? 0} open tickets this week`);
// SLA rules
const { data: rules } = await cstar.sla.list();
// Cheer on another team. The sender defaults to your API key's creator.
const highFive = await cstar.game.highFives.send({
recipientTeamId: '550e8400-e29b-41d4-a716-446655440003',
message: 'Amazing work!'
});
console.log(`A high five from ${highFive.senderTeam.name}`);
// AI-powered reply suggestion
const suggestion = await cstar.ai.suggestReply('tkt_abc123');
// Bulk close resolved tickets
await cstar.bulk.update({
resource: 'tickets',
ids: ['tkt_1', 'tkt_2', 'tkt_3'],
data: { status: 'closed' }
});
// Export closed tickets as JSON with raw rows and per-resource counts
const exported = await cstar.export.create({
resources: ['tickets'],
filters: { status: 'closed' },
format: 'json'
});
console.log(exported.summary.tickets);
// CSV is text and supports one resource at a time
const csv = await cstar.export.create({ resources: ['tickets'], format: 'csv' });
// Saved views
const view = await cstar.views.create({
name: 'Critical Unresolved',
entityType: 'tickets',
filters: { priority: 'critical', status: 'open' }
});
const { data: results } = await cstar.views.execute(view.id);
// Search across everything
const { results: searchResults } = await cstar.search.query({
query: 'billing issue',
types: ['tickets', 'customers']
});
Client-Side: Chat Widget SDK
Build a chat experience that feels at home on your site. ChatClient handles customer conversations in the browser without a team API key.
The chat client supports two authentication modes:
- HMAC: your authenticated backend signs the customer's identity with the team's signing secret. Use this when your site already knows who's signed in.
- Session: sign in with cStar customer credentials, or provide an existing cStar customer access token. Use this for a standalone widget or customer portal.
Choose the flow supported by your team's widget configuration. chat.getAuthMode() returns 'hmac', 'session', or null before authentication or after disconnecting. A session login sets the session mode; the SDK also reads the server's X-Cstar-Auth-Mode response header.
HMAC
Create /api/support/identity on your own backend. It must derive the customer from the authenticated user and return { customer, signature } using your team's signing secret. Send that exact payload to cStar; generating a new timestamp in the browser changes the signed data. The identity guide shows the signing contract.
import { ChatClient, type IdentifyParams } from '@cstar.help/js/chat';
const chat = new ChatClient({ teamSlug: 'acme' });
const response = await fetch('/api/support/identity', { credentials: 'same-origin' });
if (!response.ok) throw new Error('Could not verify your support identity');
const { customer, signature } = (await response.json()) as {
customer: IdentifyParams;
signature: string;
};
await chat.identify(customer, signature);
Session (email/password)
import { ChatClient } from '@cstar.help/js/chat';
const chat = new ChatClient({ teamSlug: 'acme' });
// Use the values from your customer's sign-in form.
const { customer, teamId } = await chat.loginWithCredentials({
email: '[email protected]',
password: 'your-customer-password'
});
console.log(`Hello, ${customer.name ?? customer.email}!`, teamId);
The client keeps the access token internally. The public result contains customer and teamId, not access or refresh tokens. This signs in an existing customer; it does not create an account.
Session (existing customer token)
Pass an existing cStar customer token as an object. A token from an unrelated Supabase project does not establish a cStar customer session. Include the cStar team UUID when your application has it. Otherwise the client resolves its configured team slug before message, community and realtime requests.
import { ChatClient, type LoginWithSessionParams } from '@cstar.help/js/chat';
async function connectCustomerSession(session: LoginWithSessionParams) {
const chat = new ChatClient({ teamSlug: 'acme' });
await chat.loginWithSession(session); // { accessToken, teamId?, customerName? }
return chat;
}
Once authenticated (either mode)
const { data: tickets, pagination } = await chat.tickets.list();
console.log(`${tickets.length} conversations`, pagination.hasMore);
// Use a ticket ID returned by the customer API.
const ticket = tickets[0];
let unsubscribe = () => {};
if (ticket) {
const history = await chat.messages.list(ticket.id);
console.log(history); // Message[]
await chat.messages.send(ticket.id, 'Thanks for the help!');
unsubscribe = chat.onMessage(ticket.id, (message) => {
console.log(`${message.sender_name}: ${message.content}`);
});
}
// Wire this to your UI's close/unmount handler and account-change cleanup.
function closeChat() {
unsubscribe();
chat.disconnect();
}
Typed errors
HTTP and network failures use typed subclasses of ChatClientError. A server response may provide a requestId through the X-Request-Id header; it is null when unavailable. Other failures, such as malformed response JSON, can still throw ordinary errors.
Customer message sends generate a new retry key for each intentional reply and retry network or server failures at most twice. Each retry uses the same key and returns the original saved reply. A terminal ChatClientError retains that key in error.idempotencyKey. Reuse it with chat.messages.send(ticketId, content, { idempotencyKey }) for a manual retry of the same reply. Keep the draft and key together; use a new key when the message or ticket changes. Receipts last 24 hours and stay scoped to the customer, team and live/test mode.
Pass { signal } to cancel a send or its retry delay. For a cancellable UI, generate and retain your own idempotencyKey before sending. A cancelled request may already have been saved. Conversation history includes client_request_key so your UI can reconcile a pending bubble with its confirmed reply. Separate sends with identical text remain separate messages. These message retries do not apply to ticket creation or other customer writes.
import {
ChatClientError,
ChatClientAuthError,
ChatClientRateLimitError,
ChatClientServerError,
ChatClientNetworkError
} from '@cstar.help/js/chat';
try {
await chat.identify(customer, signature);
} catch (err) {
if (err instanceof ChatClientAuthError) {
// Authentication failed or the active customer session changed.
} else if (err instanceof ChatClientRateLimitError) {
// 429: respect err.retryAfter when the server supplies it.
} else if (err instanceof ChatClientServerError) {
// Server failure: keep the draft and check the conversation.
} else if (err instanceof ChatClientNetworkError) {
// Network failure: err.originalError has the underlying cause.
} else if (err instanceof ChatClientError) {
console.log('Error', err.code, err.statusCode, 'requestId:', err.requestId);
} else {
throw err;
}
}
Client-Side: Public Knowledge Base
Search and browse your public help center. No authentication required.
import { LibraryClient } from '@cstar.help/js/library';
const library = new LibraryClient({ teamSlug: 'acme' });
const categories = await library.categories();
const results = await library.search('reset password');
const article = await library.article('how-to-reset-password');
Client-Side: Public Community
Browse your public community forum. No authentication required.
import { CommunityClient } from '@cstar.help/js/community';
const community = new CommunityClient({ teamSlug: 'acme' });
const topics = await community.topics();
const { posts } = await community.posts({ topicSlug: 'feature-requests', sort: 'votes' });
const { post, comments } = await community.post('dark-mode-support');
Server-Side: Webhook Verification
Verify incoming webhook signatures on your server.
import { constructEvent } from '@cstar.help/js/webhook/node';
// In your webhook handler (Node). It throws if the signature is invalid.
const event = constructEvent(rawBody, signature, 'whsec_your_secret');
switch (event.type) {
case 'ticket.created':
console.log('New ticket:', event.data.ticket.title);
break;
case 'customer.created':
console.log('New customer:', event.data.customer.email);
break;
}
For edge runtimes (Cloudflare Workers, Deno), use the async variant:
import { constructEventAsync } from '@cstar.help/js/webhook';
const event = await constructEventAsync(rawBody, signature, secret);
Configuration
const cstar = new CStarClient({
apiKey: 'sk_live_...', // Required: must be a secret key (sk_*)
teamId: '8f14e45f-ea2b-4c8b-9f1e-2a6b3c4d5e6f', // Required: your team ID (a UUID; GET /api/v1/auth/whoami returns it)
baseUrl: 'https://...', // Optional: defaults to https://www.cstar.help
version: '2026-03-01', // Optional: API version header
maxRetries: 3, // Optional: retry attempts for retry-safe failures and 429
timeout: 30000, // Optional: request timeout in ms
fetch: myFetch, // Optional: send requests through this instead of the global fetch
realtime: { types: ['ticket.created', 'message.created'] } // Optional: realtime stream options (types, eventSource polyfill, autoReconnect, maxRetries)
});
Reads retry network errors and server errors automatically. POST and PATCH
requests retry those failures only when you pass idempotencyKey in the existing
request options. A lost response may follow a successful write, so the SDK
otherwise surfaces the error without repeating the mutation. Rate-limited
requests still retry because the API rejects them before executing the operation.
await cstar.tickets.create(
{ title: 'Cannot reset password', customerId: 'cus_...' },
{ idempotencyKey: 'support-issue-123' }
);
Test Mode
Test mode is auto-detected from your API key prefix:
const cstar = new CStarClient({
apiKey: 'sk_test_abc123...',
teamId: process.env.CSTAR_TEAM_ID! // a UUID
});
console.log(cstar.isTestMode); // true
// All operations use test data, so no real customers are affected
Error Handling
All API errors throw typed exceptions:
import {
CStarError,
CStarAuthenticationError,
CStarConflictError,
CStarRateLimitError,
CStarNotFoundError,
CStarValidationError
} from '@cstar.help/js';
try {
await cstar.tickets.get('tkt_nonexistent');
} catch (err) {
if (err instanceof CStarNotFoundError) {
console.log('Ticket not found:', err.message);
} else if (err instanceof CStarValidationError) {
console.log('Invalid param:', err.param);
} else if (err instanceof CStarRateLimitError) {
console.log('Rate limited, retry after:', err.retryAfter, 'seconds');
} else if (err instanceof CStarConflictError) {
// A unique-key conflict, an Idempotency-Key still in use, or the team's
// webhook limit (code 'webhook_limit_reached': delete unused webhooks first).
console.log('Conflict:', err.code);
} else if (err instanceof CStarAuthenticationError) {
console.log('Bad API key');
} else if (err instanceof CStarError) {
console.log('API error:', err.code, err.statusCode);
}
}
Idempotency
Safely retry create/update operations:
const ticket = await cstar.tickets.create(
{ title: 'Billing issue', customerId: 'cus_abc' },
{ idempotencyKey: 'create-billing-issue-usr123' }
);
For 24 hours, the same request with the same key returns the first successful
response. The same key with a different body throws CStarValidationError
(idempotency_key_reused, 422). While the first request is still running, the
API answers 409 idempotency_key_in_use with Retry-After. From 0.18.1, the client waits and retries within maxRetries; 0.18.0 throws
CStarConflictError.
The API ignores the key on webhooks.create, webhooks.listen and
export.create, so a key does not make their retries safe. From 0.18.2 the
client does not send the key on those calls and does not retry a network error
or 5xx; 0.18.1 and earlier still retry them, so there leave the key off (without
one the client does not retry them) or use a client with maxRetries: 0. Check
webhooks.list() before you retry webhooks.create. A repeated export.create
only reads your data again (nothing is stored). A repeated webhooks.listen is a
second CLI listener: remove it with webhooks.unlisten(id), or it is swept after
5 minutes without a heartbeat.
Requirements
- Node.js 22+ (matching the supported Supabase dependency)
- TypeScript 5.0+ (for type definitions)
License
MIT