@clocknext/sdk 0.14.0 → 0.15.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"
135
-
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.");
139
-
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
- });
142
+ // 1. The model you enabled under Settings → Models. Use its id VERBATIM.
143
+ const model = "gpt-4o";
144
+
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.");
150
148
 
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,33 @@ 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
+ Every signal method can take `externalId` (your own id for the customer — see
197
+ [Your own customer ids](#your-own-customer-ids)) instead of `customerId`.
198
+
203
199
  ### Where each value comes from
204
200
 
205
201
  | Field | Source | Notes |
206
202
  | --- | --- | --- |
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. |
203
+ | `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"`. |
204
+ | `agentKey` (credit) | `customers.balances(id)` → `credits[].agentKey` | Case-insensitive. |
205
+ | `agentKey` (outcome) | `customers.balances(id)` → `outcomes[].steps[].agentKey` | The **step's** key, not the outcome's own `agentKey`. |
206
+ | `agentKey` (unit) | `customers.balances(id)` → `units[].agentKey` | Matched **exactly** — keys are stored lowercased. |
211
207
  | `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. |
208
+ | `composite.ref` | the composite's **Ref ID** in the dashboard | An unknown `ref` is ignored, not rejected — the signal still records. |
213
209
 
214
210
  ### Credit and wallet
215
211
 
216
212
  ```ts
217
213
  await cnk.signals.credit({
218
214
  customerId,
219
- model: model.modelId,
215
+ model,
220
216
  agentKey: credit.agentKey,
221
217
  tokens: { input: 500, output: 120 },
222
218
  });
223
219
 
224
220
  await cnk.signals.wallet({
225
221
  customerId,
226
- model: model.modelId,
222
+ model,
227
223
  tokens: { input: 500, output: 120 },
228
224
  });
229
225
  ```
@@ -243,7 +239,7 @@ const runId = "ticket_9021";
243
239
 
244
240
  await cnk.signals.outcome({
245
241
  customerId,
246
- model: model.modelId,
242
+ model,
247
243
  agentKey: "resolved_ticket.triage", // a STEP key
248
244
  runId,
249
245
  tokens: { input: 600, output: 150 },
@@ -251,7 +247,7 @@ await cnk.signals.outcome({
251
247
 
252
248
  await cnk.signals.outcome({
253
249
  customerId,
254
- model: model.modelId,
250
+ model,
255
251
  agentKey: "resolved_ticket.resolve",
256
252
  runId,
257
253
  tokens: { input: 500, output: 200 },
@@ -288,7 +284,7 @@ const composite = { ref: "voice_ai", value: "call_5f3a" };
288
284
 
289
285
  await cnk.signals.credit({
290
286
  customerId,
291
- model: model.modelId,
287
+ model,
292
288
  agentKey: credit.agentKey,
293
289
  tokens: { input: 100, output: 50 },
294
290
  composite,
@@ -326,7 +322,7 @@ immediately.
326
322
  ```ts
327
323
  const signal = {
328
324
  customerId,
329
- model: model.modelId,
325
+ model,
330
326
  agentKey: credit.agentKey,
331
327
  tokens: { input: 1200, output: 340 },
332
328
  };
@@ -379,22 +375,60 @@ at-least-once redelivery:
379
375
  ```ts
380
376
  await cnk.signals.credit({
381
377
  customerId,
382
- model: model.modelId,
378
+ model,
383
379
  agentKey: credit.agentKey,
384
380
  tokens: { input: 10, output: 5 },
385
381
  idempotencyKey: `llm-request:${requestId}`,
386
382
  });
387
383
  ```
388
384
 
385
+ Every signal accepts `idempotencyKey` — `credit`, `wallet`, `outcome`,
386
+ `completeOutcome`, `unit` and `completeComposite`:
387
+
388
+ ```ts
389
+ await cnk.signals.unit({ customerId, agentKey: "seats", quantity: 3, idempotencyKey: `seat-sync:${syncId}` });
390
+ await cnk.signals.completeComposite({ customerId, composite, idempotencyKey: `call-done:${callId}` });
391
+ ```
392
+
389
393
  When a signal fails permanently, the key it was sent with is available on both
390
394
  the error (`error.idempotencyKey`) and the signal handed to `onError`/`onDrop`.
391
395
  **Replay with that key** — generating a new one bills the event twice.
392
396
 
397
+ ### Balance adjustments
398
+
399
+ Adjustments (`customers.adjustCredit()` and friends) work like signals: every
400
+ call gets an idempotency key automatically (a UUID, generated once per call and
401
+ reused across its retries), so a network blip never adjusts twice. Pass your own
402
+ `idempotencyKey` — your order / top-up id — to also dedupe **your own** retries
403
+ across restarts or redelivery, which an auto key can't survive.
404
+
405
+ ```ts
406
+ const res = await cnk.customers.adjustCredit(customerId, {
407
+ agentKey: "api_credit",
408
+ delta: 500,
409
+ idempotencyKey: `topup:${orderId}`,
410
+ });
411
+
412
+ if (res.replayed) {
413
+ // An earlier call with this key already added the 500 (at res.adjustedAt).
414
+ // Nothing was added this time; res.remaining is the balance now.
415
+ }
416
+ ```
417
+
418
+ - Same key, same request → nothing is adjusted again; `replayed: true`,
419
+ `adjustedAt` is when the original happened, `remaining` is the current balance.
420
+ - Same key, different request (another customer, entitlement or amount) →
421
+ `ConflictError`, nothing adjusted. Use a new key for a new adjustment.
422
+ - A failed adjustment (validation, not on the plan, …) does not use up its key —
423
+ fix the request and send it again with the same key.
424
+ - Keys are unique per workspace and per adjust endpoint (credits, outcomes,
425
+ units, composites, wallet). 1–255 characters.
426
+
393
427
  Retries apply to network failures, timeouts, `408`, `429`, `5xx`, and the one
394
428
  transient `409`. Validation, authentication, not-found, plan, allowance and
395
429
  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.
430
+ `POST`/`PATCH` are not, except metering calls and balance adjustments, which
431
+ opt in because they are always idempotency-keyed.
398
432
 
399
433
  Client-side batching is timing and concurrency management, not a bulk API — each
400
434
  signal is still an individual HTTP request.
@@ -487,7 +521,7 @@ const cnk = new ClockNext({
487
521
  `onFlush` counts signals the server **accepted**, not signals it billed.
488
522
 
489
523
  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
524
+ reach ClockNext; `signals.usageLog()` tells you what got priced; the dashboard's
491
525
  signal log tells you why an individual signal did not.
492
526
 
493
527
  ## Balances and usage
@@ -505,18 +539,33 @@ await cnk.customers.creditBalance(customerId, { creditName: "API credit" });
505
539
  await cnk.customers.outcomeBalance(customerId, { outcomeName: "Resolved ticket" });
506
540
  await cnk.customers.unitBalance(customerId, { unitName: "Seats" });
507
541
 
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 });
542
+ // Usage history across every meter, newest first, cursor-paginated.
543
+ let cursor: string | undefined;
544
+ do {
545
+ const page = await cnk.signals.usageLog({ customerId, limit: 50, cursor });
546
+ // page.usageLogs — each row has a `kind`: credit | outcome | unit | wallet | composite
547
+ cursor = page.nextCursor ?? undefined;
548
+ } while (cursor);
511
549
  ```
512
550
 
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.
551
+ Adjustments use stable agent keys, not display names. **A credit, outcome, unit
552
+ or composite can only be adjusted when it is a prepaid (ADVANCE) part of the
553
+ customer's current plan** — the same set `customers.balances()` shows.
554
+ Otherwise the call fails with a `PlanError` ("… is not part of this customer's
555
+ plan") and nothing is written. That includes an entitlement that is only
556
+ billed in arrears, one that is only covered through a composite, and a
557
+ customer with no plan (or a cancelled one). **The wallet can always be
558
+ adjusted**, whatever the plan.
559
+
560
+ Every adjust call returns `{ remaining, replayed, customerArchived }` (plus
561
+ `adjustedAt` on a replay — see [Balance adjustments](#balance-adjustments)). A
562
+ reduce is never rejected or floored for going below 0 — the resulting balance
563
+ may read negative, same as a real-usage overdraft already can.
564
+ `customerArchived: true` means the customer is archived; the same rules apply
565
+ (archiving cancels their plan, so usually only the wallet can still move).
517
566
 
518
567
  ```ts
519
- await cnk.customers.adjustCredit(customerId, { agentKey: "api_credit", delta: 100 });
568
+ await cnk.customers.adjustCredit(customerId, { agentKey: "api_credit", delta: 100, idempotencyKey: `grant:${grantId}` });
520
569
  await cnk.customers.adjustOutcome(customerId, { agentKey: "resolved_ticket.triage", delta: 1 });
521
570
  await cnk.customers.adjustUnit(customerId, { agentKey: "seats", delta: 5 });
522
571
 
@@ -524,6 +573,39 @@ await cnk.customers.adjustUnit(customerId, { agentKey: "seats", delta: 5 });
524
573
  // value signals tag it with — named `agentKey` on the wire so all four share
525
574
  // one request shape. Adjusts the CURRENT billing cycle's ADVANCE slot pool.
526
575
  await cnk.customers.adjustComposite(customerId, { agentKey: "voice_ai", delta: 2 });
576
+
577
+ await cnk.customers.adjustWallet(customerId, { delta: 25 }); // USD, no agentKey
578
+ ```
579
+
580
+ ### Reserved Wallet
581
+
582
+ Some workspaces turn on a **Reserved Wallet**: a second balance that sits behind
583
+ the customer's ordinary wallet (Settings → Organisation → "Enable Reserved
584
+ balance in wallet"). When a wallet-funded Arrear charge comes in, it draws from
585
+ the Wallet first, then from the Reserved Wallet down to 0, and anything still
586
+ left pushes the Wallet negative. The Reserved Wallet itself never goes
587
+ negative, and a normal (`MAIN`) wallet adjustment never touches it.
588
+
589
+ ```ts
590
+ // Top up (or, with a negative delta, reduce) the Reserved Wallet.
591
+ const { remaining } = await cnk.customers.adjustWallet(customerId, { delta: 500, pool: "RESERVED" });
592
+ // `remaining` is the Reserved balance. Reducing below 0 is refused with an
593
+ // AllowanceError (down to exactly 0 is fine); any RESERVED call when the
594
+ // workspace hasn't enabled the feature is a PlanError.
595
+
596
+ const { reservedWallet } = await cnk.customers.balances(customerId); // { balanceUsd } | null
597
+ ```
598
+
599
+ `reservedWallet` is `null` when the feature is off.
600
+
601
+ To change how much a customer is granted **every billing cycle** (not just
602
+ their balance right now), use `updateRecurringGrant`. Any increase or decrease
603
+ applies to their balance immediately. `quantity` is USD for the wallet and a
604
+ whole number for everything else; the wallet needs no `agentKey`.
605
+
606
+ ```ts
607
+ await cnk.customers.updateRecurringGrant(customerId, { kind: "wallet", quantity: 10000 });
608
+ await cnk.customers.updateRecurringGrant(customerId, { kind: "credit", agentKey: "api_credit", quantity: 5000 });
527
609
  ```
528
610
 
529
611
  ## Customers, plans, and billing
@@ -537,6 +619,10 @@ for await (const c of cnk.customers.iterate()) {
537
619
  // follows the cursor for you
538
620
  }
539
621
 
622
+ // `{ active: true }` returns only active plans, `{ active: false }` only
623
+ // archived ones, and omitting it returns both.
624
+ const [plan] = await cnk.plans.list({ active: true });
625
+
540
626
  const purchase = await cnk.purchases.create({ customerId: customer.id, planId: plan.id });
541
627
  await cnk.purchases.setAutoPayment(purchase.id, false, {
542
628
  pauseReason: "Customer requested manual payment",
@@ -547,90 +633,64 @@ Additional resource methods:
547
633
 
548
634
  ```text
549
635
  cnk.customers.get / update / delete / iterate
636
+ cnk.customers.getByExternalId / updateByExternalId / updateRecurringGrantByExternalId
637
+ cnk.customers.adjustCreditByExternalId / adjustOutcomeByExternalId / adjustUnitByExternalId
638
+ cnk.customers.adjustCompositeByExternalId / adjustWalletByExternalId
639
+ cnk.customers.deleteByExternalId / balancesByExternalId / planByExternalId
550
640
  cnk.customers.listMembers / addMember / updateMember / removeMember
551
- cnk.customers.wallet / addWalletEntry / usage / plan / balances
641
+ cnk.customers.balances / creditBalance / outcomeBalance / unitBalance / plan
642
+ cnk.customers.adjustCredit / adjustOutcome / adjustUnit / adjustComposite / adjustWallet
643
+ cnk.customers.updateRecurringGrant
644
+
645
+ cnk.signals.credit / wallet / outcome / unit / track
646
+ cnk.signals.completeOutcome / completeComposite / usageLog
552
647
 
553
- cnk.plans.list / get / create / update / setActive / delete
648
+ cnk.plans.list
554
649
  cnk.purchases.list / get / create / cancel / setAutoPayment
555
650
  cnk.invoices.list / get / payLink
556
651
  cnk.payments.list / get
557
- cnk.webhooks.list / upsert / delete
558
652
  cnk.portal.createToken
559
653
  ```
560
654
 
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.
655
+ ### Your own customer ids
564
656
 
565
- ## Catalogs and workspace discovery
566
-
567
- This is where `agentKey` and `modelId` come from.
657
+ Give a customer an `externalId` — the id **your** product already uses for
658
+ them — and you never have to store ClockNext's customer id. It is 1–32
659
+ letters, numbers or `-`, case-sensitive, and unique among your active
660
+ customers (a clash is a `ConflictError`).
568
661
 
569
662
  ```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
- ```
576
-
577
- `{ active: true }` returns only active rows, `{ active: false }` only archived
578
- ones, and omitting it returns both.
663
+ await cnk.customers.create({ name: "Acme", email: "billing@acme.com", externalId: "acme-42" });
579
664
 
580
- Creating catalog entries:
665
+ // Signals and purchases.create() take `externalId` in place of `customerId`:
666
+ await cnk.signals.credit({ externalId: "acme-42", agentKey: "api_credit", model: "gpt-4o", tokens: { input: 1200, output: 300 } });
667
+ await cnk.purchases.create({ externalId: "acme-42", planId: plan.id });
581
668
 
582
- ```ts
583
- const credit = await cnk.credits.create({
584
- name: "API credit",
585
- agentKey: "api_credit", // stable identity; a rename never changes it
586
- basePrice: 10,
587
- marginPercent: 20,
588
- pricePerCredit: 12, // recomputed server-side from basePrice × margin
589
- tokensPerCredit: 1000,
590
- });
669
+ // Read, update, or resize the recurring grant by your id:
670
+ const acme = await cnk.customers.getByExternalId("acme-42");
671
+ await cnk.customers.updateByExternalId("acme-42", { currencyCode: "USD" });
672
+ await cnk.customers.updateRecurringGrantByExternalId("acme-42", { kind: "wallet", quantity: 10000 });
591
673
 
592
- 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,
598
- steps: [
599
- { name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 },
600
- { name: "Resolve", agentKey: "resolved_ticket.resolve", basePrice: 0.5 },
601
- ],
602
- });
674
+ // Manage balances by your id too:
675
+ await cnk.customers.adjustCreditByExternalId("acme-42", { agentKey: "api_credit", delta: 500 });
676
+ await cnk.customers.adjustWalletByExternalId("acme-42", { delta: 250 });
603
677
 
604
- 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
609
- });
678
+ await cnk.customers.update(acme.id, { externalId: null }); // clear it
610
679
  ```
611
680
 
612
- Signals report against a **step's** `agentKey`, never the outcome's own.
613
-
614
- Other catalog methods:
681
+ If you send both, `customerId` wins and `externalId` is ignored. An unknown
682
+ `externalId` is a `NotFoundError`; on a signal (processed asynchronously) it
683
+ leaves the signal Pending with that reason — create the customer and retry.
684
+ Customer objects carry `externalId`. Everything else — `signals.usageLog()`,
685
+ the invoice, payment and purchase lists, `portal.createToken()` and members —
686
+ still takes ClockNext's id.
615
687
 
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
621
- ```
688
+ Models, credits, outcomes, units, composites and plans are created and edited in
689
+ the ClockNext dashboard, not through the SDK.
622
690
 
623
- ## Webhooks and customer portal
691
+ ## Customer portal
624
692
 
625
693
  ```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");
633
-
634
694
  const { token, expiresAt } = await cnk.portal.createToken({
635
695
  customerId,
636
696
  ttlSeconds: 600,