@clocknext/sdk
Official ClockNext SDK for Node/TypeScript. Record usage signals and manage
customers from your backend.
Server-side only. Your cnk_… API key has org-wide write access — never
ship it to a browser. For embedding the customer portal in a browser, mint a
short-lived token with cnk.portal.createToken().
Install
npm install @clocknext/sdk
Requires Node ≥ 18 (native fetch). Zero runtime dependencies.
Quick start
import { ClockNext } from "@clocknext/sdk";
const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY! });
await cnk.signals.credit({
customerId: "cus_123",
model: "llama-3.3-70b-versatile",
key: "api_credit",
member: "alice@acme.com",
tokens: { input: 1200, output: 340, cache: 0 },
custom: { feature: "summarize" },
});
await cnk.flush();
Recording signals
There are three signal types, matching ClockNext's billable primitives. Each has
a typed helper that enforces the right required fields at compile time.
model is always the catalog model id enabled in your org (matched
case-insensitively against OrgModel.modelId). Credits and outcomes are
referenced by their stable key (assigned when you create the credit /
outcome step), not by display name.
await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", key: "api_credit", tokens: { input: 1200, output: 340 } });
await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", key: "resolved_ticket.triage" });
Usage /api/v1/usage responses use the envelope
{ statusCode, statusDetail, result }; customer endpoints currently use
{ ok, … }. The SDK unwraps both for you.
Async vs sync
By default the client is in async mode: track() buffers the signal and
returns immediately ({ queued: true }), and a background flusher sends batches
on a size/interval trigger. This keeps billing telemetry off your LLM hot path.
To get the computed result back (and any real-time allowance rejection), either:
const res = await cnk.signals.credit({ … }, { wait: true });
console.log(res.usageLog);
const cnk = new ClockNext({ apiKey, mode: "sync" });
In async mode, server-side rejections (e.g. insufficient allowance, 422)
surface via the onError hook or flush(), not from the track() call.
Reliability
- Idempotency: every signal carries an idempotency key (auto-generated once,
reused across retries) so a transport retry can't double-count. Pass your own
idempotencyKey for deterministic, cross-process dedup. (Server-side dedup
enforcement is rolling out — see the SDK plan.)
- Retries: transient failures (network, timeout,
408/409/429/5xx) are
retried with exponential backoff + jitter, honoring Retry-After. Deterministic
4xx (400/401/404/422) are never retried.
- Flushing: call
await cnk.flush() before a serverless return, and
await cnk.close() on graceful shutdown to drain the buffer.
Customers
const c = await cnk.customers.create({ name: "Acme", email: "ops@acme.com" });
const one = await cnk.customers.get(c.id);
const page = await cnk.customers.list({ limit: 50, q: "ac" });
await cnk.customers.update(c.id, { notes: "VIP" });
await cnk.customers.delete(c.id);
for await (const cust of cnk.customers.iterate()) { }
await cnk.customers.usage(c.id, { limit: 50 });
await cnk.customers.balances(c.id);
await cnk.customers.plan(c.id);
await cnk.customers.revenue(c.id, { range: "30d" });
Customer portal token
const { token, expiresAt } = await cnk.portal.createToken({ customerId: "cus_123", ttlSeconds: 600 });
Errors
All failures throw typed errors you can branch on:
import { AllowanceError, AuthError, RateLimitError } from "@clocknext/sdk";
try {
await cnk.signals.credit({ … }, { wait: true });
} catch (err) {
if (err instanceof AllowanceError) {}
else if (err instanceof AuthError) {}
else if (err instanceof RateLimitError) {}
}
ValidationError (400) · AuthError (401) · NotFoundError (404) ·
PlanError (422) · AllowanceError (422) · ConflictError (409) ·
RateLimitError (429) · ServerError (5xx) · NetworkError — all extend
ClockNextError (.status, .retryable, .idempotencyKey).
Configuration
new ClockNext({
apiKey: "cnk_…",
baseUrl: "https://app.clocknext.com",
mode: "async",
timeoutMs: 10_000,
batch: { maxSize: 20, maxIntervalMs: 2000, maxConcurrency: 5, maxQueueSize: 10_000 },
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000 },
onError: (err, signal) => {},
onFlush: (count) => {},
onRetry: ({ attempt, delayMs }) => {},
onDrop: (signal, reason) => {},
});
License
MIT