A lightweight, zero-dependency TypeScript client for the CommissionSight API.
CommissionSight ingests carrier commission statements (CSV/XLSX), normalizes them across carriers, and scores every member period-over-period as 🟢 green / 🟡 yellow / 🔴 red with explicit change flags — so you can see new business, commission changes, and attrition at a glance. This SDK wraps the REST API with full type definitions.
- Zero runtime dependencies — just
fetch. - Fully typed — every request and response is described by an exported interface.
- ESM-only — modern
importsyntax (Node 18+, Bun, Deno, browsers, Cloudflare Workers). - Isomorphic — pass your own
fetchfor non-standard runtimes or tests.
npm install @commissionsight/sdk
# or
bun add @commissionsight/sdk
# or
pnpm add @commissionsight/sdkESM-only. This package ships
type: "module"and only exposes animportentry point. Useimport, notrequire(). Requires a runtime with a globalfetch(Node 18+).
import { CommissionSightClient } from '@commissionsight/sdk';
const cs = new CommissionSightClient({
baseUrl: 'https://api.commissionsight.com/v1',
token: process.env.COMMISSIONSIGHT_TOKEN, // a per-account API token
});
const carriers = await cs.listCarriers();
console.log(carriers.data); // [{ id, name, slug }, ...]interface ClientOptions {
baseUrl: string; // e.g. https://api.commissionsight.com/v1
token?: string; // Bearer token; can also be set later via setToken()
fetch?: typeof fetch; // custom fetch (defaults to globalThis.fetch)
}baseUrl may include or omit a trailing slash — it's normalized. Set or rotate the token at any time:
cs.setToken(newToken);
cs.setToken(undefined); // clear itThe SDK is for server-to-server integrations, authenticated with a per-account API token
issued to you by CommissionSight. Pass it as token when constructing the client (or set/rotate it
later); every request is sent as Authorization: Bearer <token>.
const cs = new CommissionSightClient({
baseUrl: 'https://api.commissionsight.com/v1',
token: process.env.COMMISSIONSIGHT_TOKEN, // your per-account API token
});
cs.setToken(newToken); // rotate at any timeUploading a file kicks off an asynchronous ingest job. Poll the job until it's completed, then read the scored results.
// `file` is a File or Blob — e.g. from an <input type="file"> or fs in Node.
const { jobId, fileId } = await cs.uploadFile({
file,
carrierId: 'car_123',
periodYear: 2026,
periodMonth: 5,
// Optional: get a signed webhook callback when the job finishes.
webhookUrl: 'https://acme.com/hooks/commissionsight',
// Optional: safe retries — re-uploading with the same key won't double-ingest.
idempotencyKey: 'acme-2026-05-aetna',
});
// Poll until done.
let job = await cs.getJob(jobId);
while (job.status === 'queued' || job.status === 'processing') {
await new Promise((r) => setTimeout(r, 1500));
job = await cs.getJob(jobId);
}
if (job.status === 'failed') throw new Error(job.error ?? 'ingest failed');
// Read the scored rows for this period.
const results = await cs.getJobResults(jobId, { status: 'yellow' });
for (const row of results.data) {
console.log(row.memberRefId, row.status, row.flags, row.commissionAmount);
}If you upload an earlier month after a later one, the later period's scoring becomes stale. listFiles() flags this with rescoreSuggested; refresh it without re-uploading:
const files = await cs.listFiles({ carrierId: 'car_123' });
for (const f of files.data) {
if (f.rescoreSuggested) await cs.rescoreFile(f.id);
}Uploading over a carrier+period that already has a file fails with 409 (period_exists) — re-uploads are never silently destructive. To apply a corrected file, pass replace: true: the existing data is retracted and the corrected file re-ingested atomically, so members the correction drops leave no orphan rows. The following month is re-scored automatically.
// Carrier sent a corrected April file — replace the one on record.
const { jobId, mode } = await cs.uploadFile({
file: correctedFile,
carrierId: 'car_123',
periodYear: 2026,
periodMonth: 4,
replace: true, // omit → 409 period_exists if the period already exists
});
// mode === 'replace'
// Or remove a period entirely (no re-upload), re-scoring the next month:
await cs.retractFile(fileId);Every scored member row carries a status and zero or more flags:
status |
Meaning |
|---|---|
🟢 green |
Present and unchanged vs. the prior period. |
🟡 yellow |
Present but something tracked changed (see flags). |
🔴 red |
Present in the prior period, absent now (dropped). |
flag |
Meaning |
|---|---|
NEW |
First time this member is seen. |
COMMISSION_CHANGED |
Commission amount differs from the prior period. |
DATA_CHANGED |
A tracked non-commission field changed. |
DROPPED |
Was present before, missing now. |
REAPPEARED |
Returned after being absent. |
REAPPEARED_WITH_DELTA |
Returned and came back with a different commission. |
import type { Status, Flag, ResultRow } from '@commissionsight/sdk';// Files & jobs
await cs.listFiles({ carrierId, limit: 50 });
await cs.listJobs({ status: 'completed' });
await cs.getJobResults(jobId, { status: 'red', limit: 100, offset: 0 });
await cs.getJobDeltas(jobId, { changeType: 'COMMISSION_CHANGED' });
await cs.retryJob(jobId);
// Members & policies — status, timeline, and the full audit journey
await cs.listMembers({ carrierId, status: 'yellow' });
await cs.getMemberTimeline(memberRefId);
await cs.getMemberJourney(memberRefId); // every period, source file, status + field changes
await cs.getPolicyJourney(policyRefId);
// Rejected rows from an ingest (exception file, as CSV text)
const csv = await cs.downloadExceptions(jobId);
// Carriers & their mapping configs
await cs.listCarriers({ withConfig: true });
await cs.listConfigs(carrierId);Set your contracted rate per carrier; the API computes the recoverable shortfall vs. what the carrier actually paid (it's also a per-member field on the results grid).
await cs.upsertExpectedRate({ carrierId, rateType: 'percent_of_premium', rateValue: 0.2 });
const rollup = await cs.rollup('2026-05', carrierId);
console.log(rollup.totals.commissionOwed, rollup.totals.owedEvaluated, rollup.totals.owedTotal);const cb = await cs.listChargebacks({ carrierId }); // negative-commission events + original payoutSubscribe to signed callbacks instead of polling:
await cs.createWebhook({ url: 'https://acme.com/hooks/cs', events: ['job.completed'] });await cs.listAudit({ limit: 50 }); // append-only record of account actionsconst cmp = await cs.compare({ from: '2026-04', to: '2026-05', carrierId });
console.log(cmp.summary); // { green, yellow, red, new, reappeared, total }await cs.rollup('2026-05', carrierId); // period totals by status + by carrier
await cs.attrition('2026-05', carrierId); // attrition rate for a period
await cs.attritionSeries({ months: 12 }); // attrition trend
await cs.dataQuality('2026-05'); // statement-quality signals (ok/watch/alert)List endpoints return a Page<T>:
interface Page<T> {
data: T[];
pagination?: {
limit: number;
offset?: number;
nextCursor?: number | null;
hasMore: boolean;
};
}Offset-based endpoints accept { limit, offset }; cursor-based ones (e.g. listFiles) accept { limit, cursor } and return nextCursor.
Any non-2xx response throws an ApiError. It carries the HTTP status and the parsed RFC 9457 problem+json body when present.
import { ApiError } from '@commissionsight/sdk';
try {
await cs.getJob('does-not-exist');
} catch (err) {
if (err instanceof ApiError) {
console.error(err.status); // e.g. 404
console.error(err.message); // problem `title`
console.error(err.body); // full problem+json payload
}
}Every payload is exported as a named type — ResultRow, ComparisonRow, JobSummary, FileSummary, Journey, ChargebackRow, ExpectedCommissionRate, AuditEvent, DataQualityReport, AttritionPoint, and the Status / Flag unions. Import what you need:
import type {
CommissionSightClient,
ResultRow,
JobSummary,
Journey,
Status,
Flag,
} from '@commissionsight/sdk';- Website: https://commissionsight.com
- API docs: https://docs.commissionsight.com
- Issues: https://github.com/commissionsight/sdk/issues
MIT © CommissionSight