@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 +274 -128
- package/dist/index.cjs +345 -281
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +689 -773
- package/dist/index.d.ts +689 -773
- package/dist/index.js +345 -281
- package/dist/index.js.map +1 -1
- package/examples/node-basic.ts +24 -32
- package/package.json +1 -1
- package/src/client.ts +15 -23
- package/src/customer-ref.ts +14 -0
- package/src/errors.ts +10 -2
- package/src/filters.ts +1 -1
- package/src/index.ts +32 -58
- package/src/resources/composites.ts +55 -20
- package/src/resources/credits.ts +40 -35
- package/src/resources/customers.ts +201 -82
- package/src/resources/invoices.ts +1 -1
- package/src/resources/outcomes.ts +40 -41
- package/src/resources/plans.ts +38 -32
- package/src/resources/signals.ts +8 -46
- package/src/resources/units.ts +37 -37
- package/src/types.ts +549 -619
- package/src/resources/webhooks.ts +0 -37
- package/src/resources/workspace.ts +0 -33
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,
|
|
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
|
|
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
|
|
43
|
-
| `agentKey` names no credit / outcome step / unit | read it from `
|
|
44
|
-
| the customer has no active purchase of a plan carrying that entitlement | `plans.
|
|
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.
|
|
65
|
-
priced usage
|
|
66
|
-
whether pricing succeeded. It cannot tell you about a signal that was
|
|
67
|
-
*before* it was ever priced — an unknown `agentKey`, a customer with no
|
|
68
|
-
because no usage
|
|
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
|
|
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(
|
|
76
|
+
console.log(page.usageLogs.length);
|
|
75
77
|
|
|
76
78
|
// Present but not billed → priced and refused; `status` says which.
|
|
77
|
-
for (const
|
|
78
|
-
if (
|
|
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 →
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
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.
|
|
133
|
-
const
|
|
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
|
|
137
|
-
const [
|
|
138
|
-
if (!
|
|
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
|
|
141
|
-
|
|
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
|
-
//
|
|
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
|
|
165
|
+
model,
|
|
173
166
|
agentKey: credit.agentKey,
|
|
174
167
|
tokens: { input: 1200, output: 340 },
|
|
175
168
|
custom: { feature: "summarize" },
|
|
176
169
|
});
|
|
177
170
|
|
|
178
|
-
//
|
|
171
|
+
// 6. Required in async mode before the process can exit.
|
|
179
172
|
await cnk.flush();
|
|
180
173
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
console.log(
|
|
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` |
|
|
208
|
-
| `agentKey` (credit) | `
|
|
209
|
-
| `agentKey` (outcome) | `
|
|
210
|
-
| `agentKey` (unit) | `
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
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.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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.
|
|
660
|
+
cnk.customers.balances / creditBalance / outcomeBalance / unitBalance / plan
|
|
661
|
+
cnk.customers.adjustCredit / adjustOutcome / adjustUnit / adjustComposite / adjustWallet
|
|
662
|
+
cnk.customers.updateRecurringGrant
|
|
552
663
|
|
|
553
|
-
cnk.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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: "
|
|
585
|
-
agentKey: "
|
|
586
|
-
basePrice: 10,
|
|
726
|
+
name: "Deep Research",
|
|
727
|
+
agentKey: "deep_research",
|
|
587
728
|
marginPercent: 20,
|
|
588
|
-
|
|
589
|
-
|
|
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: "
|
|
594
|
-
agentKey: "
|
|
595
|
-
|
|
596
|
-
marginPercent: 20,
|
|
597
|
-
pricePerOutcome: 1.2,
|
|
735
|
+
name: "Contract review",
|
|
736
|
+
agentKey: "contract_review",
|
|
737
|
+
marginPercent: 100,
|
|
598
738
|
steps: [
|
|
599
|
-
{ name: "
|
|
600
|
-
|
|
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: "
|
|
606
|
-
agentKey: "
|
|
607
|
-
pricingType: "
|
|
608
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
cnk.
|
|
618
|
-
cnk.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|