@clocknext/sdk 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,8 @@ Official TypeScript/JavaScript SDK for ClockNext. Use it from a backend to:
4
4
 
5
5
  - record credit, wallet, outcome, and unit usage;
6
6
  - read customer usage, balances, plans, invoices, and payments;
7
- - manage customers, billing plans, purchases, catalogs, webhooks, and portal tokens.
7
+ - manage customers, their members and balances, purchases, and portal tokens;
8
+ - list the billing plans a customer can be subscribed to.
8
9
 
9
10
  The SDK targets the ClockNext `/api/v1` API and requires Node.js 18 or newer.
10
11
  It has zero runtime dependencies and works with native `fetch` in Node, Bun,
@@ -26,7 +27,7 @@ about to be thrown away:
26
27
 
27
28
  ```ts
28
29
  const result = await cnk.signals.credit(
29
- { customerId, model: model.modelId, agentKey: credit.agentKey, tokens: { input: 1, output: 1 } },
30
+ { customerId, model, agentKey: credit.agentKey, tokens: { input: 1, output: 1 } },
30
31
  { wait: true },
31
32
  );
32
33
  // { status: "accepted", usageLog: null, rawId: "...", messageId: "..." }
@@ -39,9 +40,9 @@ A signal is silently discarded if any of these is true:
39
40
  | Cause | How to avoid it |
40
41
  | --- | --- |
41
42
  | `customerId` does not exist | create the customer first |
42
- | `model` is not enabled in your workspace | use a `modelId` from `workspace.models()` verbatim |
43
- | `agentKey` names no credit / outcome step / unit | read it from `credits.list()`, `outcomes.get()`, `units.list()` |
44
- | the customer has no active purchase of a plan carrying that entitlement | `plans.create()` then `purchases.create()` |
43
+ | `model` is not enabled in your workspace | use a model id enabled under **Settings → Models** in the dashboard, verbatim |
44
+ | `agentKey` names no credit / outcome step / unit | read it from `customers.balances()` — every credit, outcome step and unit on the customer's plan carries its `agentKey` |
45
+ | the customer has no active purchase of a plan carrying that entitlement | pick a plan with `plans.list()`, then `purchases.create()` |
45
46
  | `member` is set to an email that is not a member of that customer | `customers.addMember()` first, or omit `member` |
46
47
  | a required field is missing | see the tables below |
47
48
  | the allowance is exhausted | top up with `customers.adjustCredit()` etc. |
@@ -61,21 +62,22 @@ reaches it, because from the SDK's point of view that request succeeded. Pass it
61
62
  anyway: it is how you catch a bad API key, an unreachable host, and a dropped
62
63
  buffer.
63
64
 
64
- **`signals.list()` is the programmatic check, and it is partial.** It returns
65
- priced usage logs, each with a `status`, so it can tell you a signal landed and
66
- whether pricing succeeded. It cannot tell you about a signal that was rejected
67
- *before* it was ever priced — an unknown `agentKey`, a customer with no plan —
68
- because no usage log is written for one. Those appear only in the dashboard.
65
+ **`signals.usageLog()` is the programmatic check, and it is partial.** It
66
+ returns priced usage rows, each with a `status`, so it can tell you a signal
67
+ landed and whether pricing succeeded. It cannot tell you about a signal that was
68
+ rejected *before* it was ever priced — an unknown `agentKey`, a customer with no
69
+ plan — because no usage row is written for one. Those appear only in the
70
+ dashboard.
69
71
 
70
72
  ```ts
71
- const usage = await cnk.signals.list({ customerId });
73
+ const page = await cnk.signals.usageLog({ customerId });
72
74
 
73
75
  // Nothing here at all → rejected before pricing. Check the dashboard's log.
74
- console.log(usage.totals.count);
76
+ console.log(page.usageLogs.length);
75
77
 
76
78
  // Present but not billed → priced and refused; `status` says which.
77
- for (const log of usage.logs) {
78
- if (log.status !== "SUCCESS") console.warn(log.id, log.status);
79
+ for (const row of page.usageLogs) {
80
+ if (row.status !== "SUCCESS") console.warn(row.id, row.status);
79
81
  }
80
82
  ```
81
83
 
@@ -85,14 +87,17 @@ A signal is not self-contained; it references rows that must already be there.
85
87
  The full chain, in order:
86
88
 
87
89
  ```
88
- enabled model → catalog entitlement (credit / outcome / unit, with an agentKey)
90
+ enabled model → entitlement (credit / outcome / unit, with an agentKey)
89
91
  → plan containing that entitlement
90
92
  → purchase linking the customer to the plan
91
93
  → customer
92
94
  ```
93
95
 
96
+ The model, the entitlements and the plan are set up in the ClockNext dashboard.
97
+ The SDK then links a customer to a plan and meters against it.
98
+
94
99
  `customerId`, `model` and `agentKey` are **not strings you can invent**. The
95
- quick start below discovers each one instead of hard-coding it.
100
+ quick start below reads each one back instead of hard-coding it.
96
101
 
97
102
  ---
98
103
 
@@ -112,8 +117,13 @@ npm install @clocknext/sdk
112
117
 
113
118
  ## Quick start — zero to a recorded signal
114
119
 
115
- This is a complete, runnable setup for an empty workspace. Every id is read back
116
- from the API rather than guessed.
120
+ Before running this, do the one-time setup in the ClockNext dashboard:
121
+
122
+ 1. Enable a model under **Settings → Models**.
123
+ 2. Create a credit (its `agentKey` is what signals address).
124
+ 3. Create a plan that sells that credit.
125
+
126
+ Everything else is read back from the API rather than guessed.
117
127
 
118
128
  ```ts
119
129
  import { ClockNext } from "@clocknext/sdk";
@@ -129,58 +139,41 @@ const cnk = new ClockNext({
129
139
  console.error("[clocknext] signal rejected", signal?.type, error),
130
140
  });
131
141
 
132
- // 1. Which workspace is this key for? `live` bills real money.
133
- const { organization } = await cnk.workspace.me();
134
- console.log(organization.environment); // "sandbox" | "live"
142
+ // 1. The model you enabled under Settings → Models. Use its id VERBATIM.
143
+ const model = "gpt-4o";
135
144
 
136
- // 2. A model you are allowed to meter against. Use `modelId` VERBATIM.
137
- const [model] = await cnk.workspace.models({ active: true });
138
- if (!model) throw new Error("Enable a model in Settings → Models first.");
145
+ // 2. A plan to subscribe the customer to.
146
+ const [plan] = await cnk.plans.list({ active: true });
147
+ if (!plan) throw new Error("Create a plan in the dashboard first.");
139
148
 
140
- // 3. A credit type. Its `agentKey` is what signals address.
141
- let [credit] = await cnk.credits.list({ active: true });
142
- credit ??= await cnk.credits.create({
143
- name: "API credit",
144
- agentKey: "api_credit", // stable, lowercase [a-z0-9._-], unique per org
145
- basePrice: 10,
146
- marginPercent: 20,
147
- pricePerCredit: 12, // server recomputes this from basePrice × margin
148
- tokensPerCredit: 1000,
149
- });
150
-
151
- // 4. A customer.
149
+ // 3. A customer, and a purchase linking them to the plan.
150
+ // Without the purchase every signal below is silently discarded.
152
151
  const customer = await cnk.customers.create({
153
152
  name: "Acme",
154
153
  email: "billing@acme.com",
155
154
  });
156
-
157
- // 5. A plan that SELLS that credit, and a purchase linking the two.
158
- // Without this step every signal below is silently discarded.
159
- const plan = await cnk.plans.create({
160
- name: "Pro",
161
- billingCycle: "MONTHLY",
162
- currencyCode: "USD",
163
- components: [
164
- { type: "CREDIT", billingMode: "ADVANCE", creditId: credit.id, quantity: 10_000 },
165
- ],
166
- });
167
155
  await cnk.purchases.create({ customerId: customer.id, planId: plan.id });
168
156
 
169
- // 6. Now a signal can land.
157
+ // 4. The credit's agentKey, read off the customer's balances.
158
+ const { credits } = await cnk.customers.balances(customer.id);
159
+ const credit = credits[0];
160
+ if (!credit) throw new Error("The plan must sell at least one credit.");
161
+
162
+ // 5. Now a signal can land.
170
163
  await cnk.signals.credit({
171
164
  customerId: customer.id,
172
- model: model.modelId,
165
+ model,
173
166
  agentKey: credit.agentKey,
174
167
  tokens: { input: 1200, output: 340 },
175
168
  custom: { feature: "summarize" },
176
169
  });
177
170
 
178
- // 7. Required in async mode before the process can exit.
171
+ // 6. Required in async mode before the process can exit.
179
172
  await cnk.flush();
180
173
 
181
- // 8. Prove it. Ingestion is asynchronous, so allow a moment.
182
- const usage = await cnk.signals.list({ customerId: customer.id });
183
- console.log(usage.totals.count, "signals recorded");
174
+ // 7. Prove it. Ingestion is asynchronous, so allow a moment.
175
+ const page = await cnk.signals.usageLog({ customerId: customer.id });
176
+ console.log(page.usageLogs.length, "signals recorded");
184
177
  ```
185
178
 
186
179
  A runnable version is in [`examples/node-basic.ts`](./examples/node-basic.ts),
@@ -200,30 +193,49 @@ Signals are billable usage events. Each kind posts to its own route.
200
193
  `signals.track(signal)` is the underlying call the four convenience methods wrap;
201
194
  use it when the kind is dynamic.
202
195
 
196
+ `tokens` takes `input` and `output` (required), plus two optional prompt-cache
197
+ counts that each default to 0:
198
+
199
+ - `cache` — tokens **read** from the prompt cache, priced at the model's cache-read rate.
200
+ - `cacheWrite` — tokens **written** into the prompt cache, priced at the model's
201
+ separate cache-write rate.
202
+
203
+ ```ts
204
+ await cnk.signals.credit({
205
+ customerId,
206
+ model: "claude-sonnet-4-5",
207
+ agentKey: "api_credit",
208
+ tokens: { input: 1200, output: 340, cache: 800, cacheWrite: 2000 },
209
+ });
210
+ ```
211
+
212
+ Every signal method can take `externalId` (your own id for the customer — see
213
+ [Your own customer ids](#your-own-customer-ids)) instead of `customerId`.
214
+
203
215
  ### Where each value comes from
204
216
 
205
217
  | Field | Source | Notes |
206
218
  | --- | --- | --- |
207
- | `model` | `workspace.models()` → `modelId` | Use it verbatim. Matching is case-insensitive but the id must exist; `"openai/gpt-4o"` is not the same as `"gpt-4o"`. |
208
- | `agentKey` (credit) | `credits.list()` → `agentKey` | Case-insensitive. |
209
- | `agentKey` (outcome) | `outcomes.get(id)` → `steps[].agentKey` | The **step's** key, not the outcome's own `agentKey`. `outcomes.list()` does not return steps. |
210
- | `agentKey` (unit) | `units.list()` → `agentKey` | Matched **exactly** — keys are stored lowercased. |
219
+ | `model` | **Settings → Models** in the dashboard | Use it verbatim. Matching is case-insensitive but the id must exist; `"openai/gpt-4o"` is not the same as `"gpt-4o"`. |
220
+ | `agentKey` (credit) | `customers.balances(id)` → `credits[].agentKey` | Case-insensitive. |
221
+ | `agentKey` (outcome) | `customers.balances(id)` → `outcomes[].steps[].agentKey` | The **step's** key, not the outcome's own `agentKey`. |
222
+ | `agentKey` (unit) | `customers.balances(id)` → `units[].agentKey` | Matched **exactly** — keys are stored lowercased. |
211
223
  | `member` | `customers.listMembers(id)` → `email` | Optional. **If set to an email that is not a member of this customer, the whole signal is discarded.** Add them with `customers.addMember()` first. |
212
- | `composite.ref` | `composites.list()` → `refId` | An unknown `ref` is ignored, not rejected — the signal still records. |
224
+ | `composite.ref` | the composite's **Ref ID** in the dashboard | An unknown `ref` is ignored, not rejected — the signal still records. |
213
225
 
214
226
  ### Credit and wallet
215
227
 
216
228
  ```ts
217
229
  await cnk.signals.credit({
218
230
  customerId,
219
- model: model.modelId,
231
+ model,
220
232
  agentKey: credit.agentKey,
221
233
  tokens: { input: 500, output: 120 },
222
234
  });
223
235
 
224
236
  await cnk.signals.wallet({
225
237
  customerId,
226
- model: model.modelId,
238
+ model,
227
239
  tokens: { input: 500, output: 120 },
228
240
  });
229
241
  ```
@@ -243,7 +255,7 @@ const runId = "ticket_9021";
243
255
 
244
256
  await cnk.signals.outcome({
245
257
  customerId,
246
- model: model.modelId,
258
+ model,
247
259
  agentKey: "resolved_ticket.triage", // a STEP key
248
260
  runId,
249
261
  tokens: { input: 600, output: 150 },
@@ -251,7 +263,7 @@ await cnk.signals.outcome({
251
263
 
252
264
  await cnk.signals.outcome({
253
265
  customerId,
254
- model: model.modelId,
266
+ model,
255
267
  agentKey: "resolved_ticket.resolve",
256
268
  runId,
257
269
  tokens: { input: 500, output: 200 },
@@ -288,7 +300,7 @@ const composite = { ref: "voice_ai", value: "call_5f3a" };
288
300
 
289
301
  await cnk.signals.credit({
290
302
  customerId,
291
- model: model.modelId,
303
+ model,
292
304
  agentKey: credit.agentKey,
293
305
  tokens: { input: 100, output: 50 },
294
306
  composite,
@@ -326,7 +338,7 @@ immediately.
326
338
  ```ts
327
339
  const signal = {
328
340
  customerId,
329
- model: model.modelId,
341
+ model,
330
342
  agentKey: credit.agentKey,
331
343
  tokens: { input: 1200, output: 340 },
332
344
  };
@@ -379,22 +391,60 @@ at-least-once redelivery:
379
391
  ```ts
380
392
  await cnk.signals.credit({
381
393
  customerId,
382
- model: model.modelId,
394
+ model,
383
395
  agentKey: credit.agentKey,
384
396
  tokens: { input: 10, output: 5 },
385
397
  idempotencyKey: `llm-request:${requestId}`,
386
398
  });
387
399
  ```
388
400
 
401
+ Every signal accepts `idempotencyKey` — `credit`, `wallet`, `outcome`,
402
+ `completeOutcome`, `unit` and `completeComposite`:
403
+
404
+ ```ts
405
+ await cnk.signals.unit({ customerId, agentKey: "seats", quantity: 3, idempotencyKey: `seat-sync:${syncId}` });
406
+ await cnk.signals.completeComposite({ customerId, composite, idempotencyKey: `call-done:${callId}` });
407
+ ```
408
+
389
409
  When a signal fails permanently, the key it was sent with is available on both
390
410
  the error (`error.idempotencyKey`) and the signal handed to `onError`/`onDrop`.
391
411
  **Replay with that key** — generating a new one bills the event twice.
392
412
 
413
+ ### Balance adjustments
414
+
415
+ Adjustments (`customers.adjustCredit()` and friends) work like signals: every
416
+ call gets an idempotency key automatically (a UUID, generated once per call and
417
+ reused across its retries), so a network blip never adjusts twice. Pass your own
418
+ `idempotencyKey` — your order / top-up id — to also dedupe **your own** retries
419
+ across restarts or redelivery, which an auto key can't survive.
420
+
421
+ ```ts
422
+ const res = await cnk.customers.adjustCredit(customerId, {
423
+ agentKey: "api_credit",
424
+ delta: 500,
425
+ idempotencyKey: `topup:${orderId}`,
426
+ });
427
+
428
+ if (res.replayed) {
429
+ // An earlier call with this key already added the 500 (at res.adjustedAt).
430
+ // Nothing was added this time; res.remaining is the balance now.
431
+ }
432
+ ```
433
+
434
+ - Same key, same request → nothing is adjusted again; `replayed: true`,
435
+ `adjustedAt` is when the original happened, `remaining` is the current balance.
436
+ - Same key, different request (another customer, entitlement or amount) →
437
+ `ConflictError`, nothing adjusted. Use a new key for a new adjustment.
438
+ - A failed adjustment (validation, not on the plan, …) does not use up its key —
439
+ fix the request and send it again with the same key.
440
+ - Keys are unique per workspace and per adjust endpoint (credits, outcomes,
441
+ units, composites, wallet). 1–255 characters.
442
+
393
443
  Retries apply to network failures, timeouts, `408`, `429`, `5xx`, and the one
394
444
  transient `409`. Validation, authentication, not-found, plan, allowance and
395
445
  permanent-conflict errors are never retried. Reads and `DELETE`s are retried;
396
- `POST`/`PATCH` are not, except metering calls, which opt in because they are
397
- idempotency-keyed.
446
+ `POST`/`PATCH` are not, except metering calls and balance adjustments, which
447
+ opt in because they are always idempotency-keyed.
398
448
 
399
449
  Client-side batching is timing and concurrency management, not a bulk API — each
400
450
  signal is still an individual HTTP request.
@@ -487,17 +537,19 @@ const cnk = new ClockNext({
487
537
  `onFlush` counts signals the server **accepted**, not signals it billed.
488
538
 
489
539
  So the division of labour is: these hooks tell you your integration cannot
490
- reach ClockNext; `signals.list()` tells you what got priced; the dashboard's
540
+ reach ClockNext; `signals.usageLog()` tells you what got priced; the dashboard's
491
541
  signal log tells you why an individual signal did not.
492
542
 
493
543
  ## Balances and usage
494
544
 
495
545
  ```ts
496
546
  const balances = await cnk.customers.balances(customerId);
497
- balances.wallet?.balanceUsd;
547
+ balances.wallet.balance; // USD; always present
548
+ balances.wallet.reservedBalance; // USD; undefined unless Reserved Wallet is on
498
549
  balances.credits; // granted, used, remaining
499
550
  balances.outcomes; // granted, remaining, steps
500
551
  balances.units; // granted, remaining
552
+ balances.composites; // prepaid composite pools: refId, granted, used, remaining
501
553
 
502
554
  // Convenience lookups — these fetch the full snapshot, so prefer `balances()`
503
555
  // when you need more than one row from the same customer.
@@ -505,18 +557,33 @@ await cnk.customers.creditBalance(customerId, { creditName: "API credit" });
505
557
  await cnk.customers.outcomeBalance(customerId, { outcomeName: "Resolved ticket" });
506
558
  await cnk.customers.unitBalance(customerId, { unitName: "Seats" });
507
559
 
508
- // Usage history, and unit balances + recent unit events.
509
- await cnk.signals.list({ customerId, limit: 50 });
510
- await cnk.signals.unitUsage({ customerId, limit: 50 });
560
+ // Usage history across every meter, newest first, cursor-paginated.
561
+ let cursor: string | undefined;
562
+ do {
563
+ const page = await cnk.signals.usageLog({ customerId, limit: 50, cursor });
564
+ // page.usageLogs — each row has a `kind`: credit | outcome | unit | wallet | composite
565
+ cursor = page.nextCursor ?? undefined;
566
+ } while (cursor);
511
567
  ```
512
568
 
513
- Adjustments use stable agent keys, not display names. All four return the
514
- resulting `{ remaining }` balance; a reduce is never rejected or floored for
515
- going below 0 — the resulting balance may read negative, same as a real-usage
516
- overdraft already can.
569
+ Adjustments use stable agent keys, not display names. **A credit, outcome, unit
570
+ or composite can only be adjusted when it is a prepaid (ADVANCE) part of the
571
+ customer's current plan** — the same set `customers.balances()` shows.
572
+ Otherwise the call fails with a `PlanError` ("… is not part of this customer's
573
+ plan") and nothing is written. That includes an entitlement that is only
574
+ billed in arrears, one that is only covered through a composite, and a
575
+ customer with no plan (or a cancelled one). **The wallet can always be
576
+ adjusted**, whatever the plan.
577
+
578
+ Every adjust call returns `{ remaining, replayed, customerArchived }` (plus
579
+ `adjustedAt` on a replay — see [Balance adjustments](#balance-adjustments)). A
580
+ reduce is never rejected or floored for going below 0 — the resulting balance
581
+ may read negative, same as a real-usage overdraft already can.
582
+ `customerArchived: true` means the customer is archived; the same rules apply
583
+ (archiving cancels their plan, so usually only the wallet can still move).
517
584
 
518
585
  ```ts
519
- await cnk.customers.adjustCredit(customerId, { agentKey: "api_credit", delta: 100 });
586
+ await cnk.customers.adjustCredit(customerId, { agentKey: "api_credit", delta: 100, idempotencyKey: `grant:${grantId}` });
520
587
  await cnk.customers.adjustOutcome(customerId, { agentKey: "resolved_ticket.triage", delta: 1 });
521
588
  await cnk.customers.adjustUnit(customerId, { agentKey: "seats", delta: 5 });
522
589
 
@@ -524,6 +591,40 @@ await cnk.customers.adjustUnit(customerId, { agentKey: "seats", delta: 5 });
524
591
  // value signals tag it with — named `agentKey` on the wire so all four share
525
592
  // one request shape. Adjusts the CURRENT billing cycle's ADVANCE slot pool.
526
593
  await cnk.customers.adjustComposite(customerId, { agentKey: "voice_ai", delta: 2 });
594
+
595
+ await cnk.customers.adjustWallet(customerId, { delta: 25 }); // USD, no agentKey
596
+ ```
597
+
598
+ ### Reserved Wallet
599
+
600
+ Some workspaces turn on a **Reserved Wallet**: a second balance that sits behind
601
+ the customer's ordinary wallet (Settings → Organisation → "Enable Reserved
602
+ balance in wallet"). When a wallet-funded Arrear charge comes in, it draws from
603
+ the Wallet first, then from the Reserved Wallet down to 0, and anything still
604
+ left pushes the Wallet negative. The Reserved Wallet itself never goes
605
+ negative, and a normal (`main`) wallet adjustment never touches it.
606
+
607
+ ```ts
608
+ // Top up (or, with a negative delta, reduce) the Reserved Wallet.
609
+ const { remaining } = await cnk.customers.adjustWallet(customerId, { delta: 500, pool: "reserved" });
610
+ // `remaining` is the Reserved balance. Reducing below 0 is refused with an
611
+ // AllowanceError (down to exactly 0 is fine); any "reserved" call when the
612
+ // workspace hasn't enabled the feature is a PlanError.
613
+
614
+ const { wallet } = await cnk.customers.balances(customerId);
615
+ wallet.reservedBalance; // number | undefined
616
+ ```
617
+
618
+ When the feature is off, `wallet.reservedBalance` is left out of `balances()`.
619
+
620
+ To change how much a customer is granted **every billing cycle** (not just
621
+ their balance right now), use `updateRecurringGrant`. Any increase or decrease
622
+ applies to their balance immediately. `quantity` is USD for the wallet and a
623
+ whole number for everything else; the wallet needs no `agentKey`.
624
+
625
+ ```ts
626
+ await cnk.customers.updateRecurringGrant(customerId, { kind: "wallet", quantity: 10000 });
627
+ await cnk.customers.updateRecurringGrant(customerId, { kind: "credit", agentKey: "api_credit", quantity: 5000 });
527
628
  ```
528
629
 
529
630
  ## Customers, plans, and billing
@@ -537,6 +638,10 @@ for await (const c of cnk.customers.iterate()) {
537
638
  // follows the cursor for you
538
639
  }
539
640
 
641
+ // `{ active: true }` returns only active plans, `{ active: false }` only
642
+ // archived ones, and omitting it returns both.
643
+ const [plan] = await cnk.plans.list({ active: true });
644
+
540
645
  const purchase = await cnk.purchases.create({ customerId: customer.id, planId: plan.id });
541
646
  await cnk.purchases.setAutoPayment(purchase.id, false, {
542
647
  pauseReason: "Customer requested manual payment",
@@ -547,90 +652,131 @@ Additional resource methods:
547
652
 
548
653
  ```text
549
654
  cnk.customers.get / update / delete / iterate
655
+ cnk.customers.getByExternalId / updateByExternalId / updateRecurringGrantByExternalId
656
+ cnk.customers.adjustCreditByExternalId / adjustOutcomeByExternalId / adjustUnitByExternalId
657
+ cnk.customers.adjustCompositeByExternalId / adjustWalletByExternalId
658
+ cnk.customers.deleteByExternalId / balancesByExternalId / planByExternalId
550
659
  cnk.customers.listMembers / addMember / updateMember / removeMember
551
- cnk.customers.wallet / addWalletEntry / usage / plan / balances
660
+ cnk.customers.balances / creditBalance / outcomeBalance / unitBalance / plan
661
+ cnk.customers.adjustCredit / adjustOutcome / adjustUnit / adjustComposite / adjustWallet
662
+ cnk.customers.updateRecurringGrant
552
663
 
553
- cnk.plans.list / get / create / update / setActive / delete
664
+ cnk.signals.credit / wallet / outcome / unit / track
665
+ cnk.signals.completeOutcome / completeComposite / usageLog
666
+
667
+ cnk.plans.list
554
668
  cnk.purchases.list / get / create / cancel / setAutoPayment
555
669
  cnk.invoices.list / get / payLink
556
670
  cnk.payments.list / get
557
- cnk.webhooks.list / upsert / delete
558
671
  cnk.portal.createToken
559
672
  ```
560
673
 
561
- Note: `plans.update()` is a full rewrite and takes the *input* shape, which is
562
- not the shape `plans.get()` returns. Rebuild the components rather than passing
563
- the read result straight back.
564
-
565
- ## Catalogs and workspace discovery
674
+ ### Your own customer ids
566
675
 
567
- This is where `agentKey` and `modelId` come from.
676
+ Give a customer an `externalId` — the id **your** product already uses for
677
+ them — and you never have to store ClockNext's customer id. It is 1–32
678
+ letters, numbers or `-`, case-sensitive, and unique among your active
679
+ customers (a clash is a `ConflictError`).
568
680
 
569
681
  ```ts
570
- const models = await cnk.workspace.models({ active: true });
571
- const credits = await cnk.credits.list({ active: true });
572
- const outcomes = await cnk.outcomes.list({ active: true });
573
- const units = await cnk.units.list({ active: true });
574
- const composites = await cnk.composites.list({ active: true });
575
- ```
682
+ await cnk.customers.create({ name: "Acme", email: "billing@acme.com", externalId: "acme-42" });
683
+
684
+ // Signals and purchases.create() take `externalId` in place of `customerId`:
685
+ await cnk.signals.credit({ externalId: "acme-42", agentKey: "api_credit", model: "gpt-4o", tokens: { input: 1200, output: 300 } });
686
+ await cnk.purchases.create({ externalId: "acme-42", planId: plan.id });
576
687
 
577
- `{ active: true }` returns only active rows, `{ active: false }` only archived
578
- ones, and omitting it returns both.
688
+ // Read, update, or resize the recurring grant by your id:
689
+ const acme = await cnk.customers.getByExternalId("acme-42");
690
+ await cnk.customers.updateByExternalId("acme-42", { currencyCode: "USD" });
691
+ await cnk.customers.updateRecurringGrantByExternalId("acme-42", { kind: "wallet", quantity: 10000 });
579
692
 
580
- Creating catalog entries:
693
+ // Manage balances by your id too:
694
+ await cnk.customers.adjustCreditByExternalId("acme-42", { agentKey: "api_credit", delta: 500 });
695
+ await cnk.customers.adjustWalletByExternalId("acme-42", { delta: 250 });
696
+
697
+ await cnk.customers.update(acme.id, { externalId: null }); // clear it
698
+ ```
699
+
700
+ If you send both, `customerId` wins and `externalId` is ignored. An unknown
701
+ `externalId` is a `NotFoundError`; on a signal (processed asynchronously) it
702
+ leaves the signal Pending with that reason — create the customer and retry.
703
+ Customer objects carry `externalId`. Everything else — `signals.usageLog()`,
704
+ the invoice, payment and purchase lists, `portal.createToken()` and members —
705
+ still takes ClockNext's id.
706
+
707
+ ## Catalogue: credits, outcomes, units, composites and plans
708
+
709
+ Each of `cnk.credits`, `cnk.outcomes`, `cnk.units`, `cnk.composites` and
710
+ `cnk.plans` has the same six calls: `list()`, `get(id)`, `create(input)`,
711
+ `update(id, input)`, `archive(id)` and `unarchive(id)`.
712
+
713
+ - **Nothing is deleted.** There is no `delete()`: retire an item with
714
+ `archive()`, bring it back with `unarchive()`. Usage history never loses
715
+ what it points at.
716
+ - **`update()` is partial** — only the fields you send change. A list you do
717
+ send (`modelBundle`, `steps`, `tiers`, `entitlements`) is the complete new
718
+ list.
719
+ - **Prices are never typed in.** A credit or outcome step sends a
720
+ `modelBundle` — one line per model — and ClockNext works the price out from
721
+ the models' live prices.
722
+ - **A plan's currency** is your workspace's primary currency; it can't be set.
581
723
 
582
724
  ```ts
583
725
  const credit = await cnk.credits.create({
584
- name: "API credit",
585
- agentKey: "api_credit", // stable identity; a rename never changes it
586
- basePrice: 10,
726
+ name: "Deep Research",
727
+ agentKey: "deep_research",
587
728
  marginPercent: 20,
588
- pricePerCredit: 12, // recomputed server-side from basePrice × margin
589
- tokensPerCredit: 1000,
729
+ modelBundle: [
730
+ { orgModelId: "cm_model_01", averageTokensPerLLMCall: 100000, input: 60, output: 30, cache: 5, cacheWrite: 5 },
731
+ ],
590
732
  });
591
733
 
592
734
  const outcome = await cnk.outcomes.create({
593
- name: "Resolved ticket",
594
- agentKey: "resolved_ticket", // the OUTCOME's key
595
- basePrice: 1,
596
- marginPercent: 20,
597
- pricePerOutcome: 1.2,
735
+ name: "Contract review",
736
+ agentKey: "contract_review",
737
+ marginPercent: 100,
598
738
  steps: [
599
- { name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 },
600
- { name: "Resolve", agentKey: "resolved_ticket.resolve", basePrice: 0.5 },
739
+ { name: "Extract clauses", agentKey: "contract_review.extract",
740
+ modelBundle: [{ orgModelId: "cm_model_01", averageTokensPerLLMCall: 50000, input: 70, output: 30, cache: 0 }] },
601
741
  ],
602
742
  });
603
743
 
604
744
  const unit = await cnk.units.create({
605
- name: "Seats",
606
- agentKey: "seats",
607
- pricingType: "FLAT",
608
- flatPrice: 5, // price per event; tiered units take `tiers` instead
745
+ name: "API calls",
746
+ agentKey: "api_calls",
747
+ pricingType: "SLAB",
748
+ tiers: [{ upTo: 1000, price: 0.01 }, { upTo: null, price: 0.005 }], // last tier: "and above"
609
749
  });
610
- ```
611
750
 
612
- Signals report against a **step's** `agentKey`, never the outcome's own.
751
+ const composite = await cnk.composites.create({
752
+ name: "Voice AI call",
753
+ refId: "voice_ai",
754
+ price: 2.5,
755
+ entitlements: { creditIds: [credit.id] },
756
+ });
613
757
 
614
- Other catalog methods:
758
+ const plan = await cnk.plans.create({
759
+ name: "Pro",
760
+ billingCycle: "MONTHLY",
761
+ entitlements: [
762
+ { type: "CREDIT", billingMode: "ADVANCE", creditId: credit.id, quantity: 1000, rollover: true },
763
+ { type: "OUTCOME", billingMode: "ARREAR", outcomeId: outcome.id },
764
+ { type: "PRICING_METRIC", billingMode: "ADVANCE", compositeId: composite.id, quantity: 50 },
765
+ { type: "WALLET", billingMode: "ADVANCE", amount: 25 },
766
+ ],
767
+ });
615
768
 
616
- ```text
617
- cnk.credits.create / get / update / setActive / delete
618
- cnk.outcomes.create / get / update / setActive / delete
619
- cnk.units.create / get / update / setActive / delete
620
- cnk.composites.list / create
769
+ await cnk.credits.update(credit.id, { marginPercent: 50 }); // only the margin changes
770
+ await cnk.units.archive(unit.id); // retire it
771
+ await cnk.units.unarchive(unit.id); // and bring it back
621
772
  ```
622
773
 
623
- ## Webhooks and customer portal
774
+ Models are enabled in the ClockNext dashboard; a bundle line's `orgModelId` is
775
+ the workspace's id for the model.
624
776
 
625
- ```ts
626
- await cnk.webhooks.upsert({
627
- kind: "WALLET", // "WALLET" | "CREDIT" | "OUTCOME"
628
- threshold: 10,
629
- url: "https://example.com/clocknext/webhook",
630
- });
631
- await cnk.webhooks.list();
632
- await cnk.webhooks.delete("WALLET");
777
+ ## Customer portal
633
778
 
779
+ ```ts
634
780
  const { token, expiresAt } = await cnk.portal.createToken({
635
781
  customerId,
636
782
  ttlSeconds: 600,