@clocknext/sdk 0.13.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 +200 -140
- package/dist/index.cjs +213 -438
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +394 -812
- package/dist/index.d.ts +394 -812
- package/dist/index.js +213 -438
- package/dist/index.js.map +1 -1
- package/examples/node-basic.ts +24 -32
- package/package.json +1 -1
- package/src/client.ts +3 -27
- package/src/customer-ref.ts +14 -0
- package/src/errors.ts +8 -1
- package/src/filters.ts +1 -1
- package/src/index.ts +13 -59
- package/src/resources/customers.ts +201 -82
- package/src/resources/invoices.ts +1 -1
- package/src/resources/plans.ts +4 -56
- package/src/resources/signals.ts +33 -38
- package/src/types.ts +301 -610
- package/src/resources/composites.ts +0 -47
- package/src/resources/credits.ts +0 -76
- package/src/resources/outcomes.ts +0 -82
- package/src/resources/units.ts +0 -76
- 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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
//
|
|
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,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` |
|
|
208
|
-
| `agentKey` (credit) | `
|
|
209
|
-
| `agentKey` (outcome) | `
|
|
210
|
-
| `agentKey` (unit) | `
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
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.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
-
##
|
|
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,
|