@clocknext/sdk 0.1.0 → 0.3.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
@@ -1,7 +1,8 @@
1
1
  # @clocknext/sdk
2
2
 
3
3
  Official ClockNext SDK for Node/TypeScript. Record **usage signals** and manage
4
- **customers** from your backend.
4
+ **plans, purchases, invoices, customers, catalogs, and billing** from your
5
+ backend — full coverage of the ClockNext `/api/v1` surface.
5
6
 
6
7
  > **Server-side only.** Your `cnk_…` API key has org-wide write access — never
7
8
  > ship it to a browser. For embedding the customer portal in a browser, mint a
@@ -13,7 +14,9 @@ Official ClockNext SDK for Node/TypeScript. Record **usage signals** and manage
13
14
  npm install @clocknext/sdk
14
15
  ```
15
16
 
16
- Requires Node ≥ 18 (native `fetch`). Zero runtime dependencies.
17
+ Requires Node ≥ 18 (native `fetch`). Zero runtime dependencies. Uses only
18
+ Web-standard `fetch` and `crypto`, so it also runs on edge runtimes (Cloudflare
19
+ Workers, Vercel Edge, Deno, Bun).
17
20
 
18
21
  ## Quick start
19
22
 
@@ -25,7 +28,7 @@ const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY! });
25
28
  await cnk.signals.credit({
26
29
  customerId: "cus_123",
27
30
  model: "llama-3.3-70b-versatile", // catalog model id (OrgModel.modelId)
28
- key: "api_credit", // the credit's stable key
31
+ agentKey: "api_credit", // the credit's stable agent key
29
32
  member: "alice@acme.com",
30
33
  tokens: { input: 1200, output: 340, cache: 0 },
31
34
  custom: { feature: "summarize" },
@@ -42,29 +45,31 @@ a typed helper that enforces the right required fields at compile time.
42
45
 
43
46
  `model` is always the **catalog model id** enabled in your org (matched
44
47
  case-insensitively against `OrgModel.modelId`). Credits and outcomes are
45
- referenced by their **stable `key`** (assigned when you create the credit /
48
+ referenced by their **stable `agentKey`** (assigned when you create the credit /
46
49
  outcome step), not by display name.
47
50
 
48
51
  ```ts
49
- // Credit — metered against a credit (by key); charged at the model's real cost + your margin
50
- await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", key: "api_credit", tokens: { input: 1200, output: 340 } });
52
+ // Credit — metered against a credit (by agentKey); charged at the model's real cost + your margin
53
+ await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit", tokens: { input: 1200, output: 340 } });
51
54
 
52
55
  // Wallet — debits the customer's USD wallet at model cost
53
56
  await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
54
57
 
55
- // Outcome — advances one step of a workflow (key = the OutcomeStep's key)
56
- await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", key: "resolved_ticket.triage" });
58
+ // Outcome — advances one step of a workflow (agentKey = the OutcomeStep's agent key)
59
+ await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", agentKey: "resolved_ticket.triage" });
57
60
  ```
58
61
 
59
- > Usage `/api/v1/usage` responses use the envelope
60
- > `{ statusCode, statusDetail, result }`; customer endpoints currently use
61
- > `{ ok, … }`. The SDK unwraps both for you.
62
-
63
62
  ### Async vs sync
64
63
 
65
64
  By default the client is in **`async`** mode: `track()` buffers the signal and
66
- returns immediately (`{ queued: true }`), and a background flusher sends batches
67
- on a size/interval trigger. This keeps billing telemetry off your LLM hot path.
65
+ returns immediately (`{ queued: true }`), and a background flusher drains the
66
+ buffer on a size/interval trigger. This keeps billing telemetry off your LLM hot
67
+ path.
68
+
69
+ > **"Batching" here means timing, not a bulk request.** Each signal is still its
70
+ > own `POST /api/v1/usage`; the flusher just groups *when* sends happen and caps
71
+ > how many run at once (`batch.maxConcurrency`). There is no single bulk-ingest
72
+ > call, so size your request budget per signal.
68
73
 
69
74
  To get the computed result back (and any real-time allowance rejection), either:
70
75
 
@@ -82,15 +87,23 @@ const cnk = new ClockNext({ apiKey, mode: "sync" });
82
87
 
83
88
  ### Reliability
84
89
 
85
- - **Idempotency:** every signal carries an idempotency key (auto-generated once,
86
- reused across retries) so a transport retry can't double-count. Pass your own
87
- `idempotencyKey` for deterministic, cross-process dedup. *(Server-side dedup
88
- enforcement is rolling out — see the SDK plan.)*
89
90
  - **Retries:** transient failures (network, timeout, `408/409/429/5xx`) are
90
- retried with exponential backoff + jitter, honoring `Retry-After`. Deterministic
91
- `4xx` (`400/401/404/422`) are never retried.
91
+ retried with exponential backoff + equal jitter, honoring `Retry-After`.
92
+ Deterministic `4xx` (`400/401/404/422`) are never retried.
93
+ - **Replay safety:** reads (`GET`), idempotent replaces (`PUT`, e.g.
94
+ `customers.setCurrency`), and `DELETE` are retried automatically. Metering
95
+ signals (`signals.*`) opt into retries too, since metering is best-effort.
96
+ Other non-idempotent writes — e.g. `customers.create`, `plans.create`,
97
+ `purchases.create` — are *not* auto-retried, so a transient blip can never
98
+ create a duplicate.
92
99
  - **Flushing:** call `await cnk.flush()` before a serverless return, and
93
100
  `await cnk.close()` on graceful shutdown to drain the buffer.
101
+ - **Durability:** the async buffer is **in-memory**. A signal already accepted
102
+ by the server is durable on the server side, but signals still buffered when a
103
+ process crashes (before send) are lost. For zero-loss-before-receipt, either
104
+ run in `sync` mode (or `{ wait: true }`) so the send completes before your
105
+ code continues, or call `flush()`/`close()` at the right boundaries. A
106
+ pluggable before-send persistence store is not yet available.
94
107
 
95
108
  ## Customers
96
109
 
@@ -111,11 +124,109 @@ await cnk.customers.plan(c.id);
111
124
  await cnk.customers.revenue(c.id, { range: "30d" });
112
125
  ```
113
126
 
127
+ ### Members
128
+
129
+ ```ts
130
+ await cnk.customers.addMember(c.id, { name: "Alice", role: "ADMIN", email: "alice@acme.com" });
131
+ const members = await cnk.customers.listMembers(c.id);
132
+ await cnk.customers.updateMember(c.id, members[0].id, { name: "Alice R." });
133
+ await cnk.customers.removeMember(c.id, members[0].id);
134
+ ```
135
+
136
+ ### Wallet & billing config
137
+
138
+ ```ts
139
+ // wallet
140
+ await cnk.customers.wallet(c.id, { limit: 50 }); // { balanceUsd, transactions }
141
+ await cnk.customers.addWalletEntry(c.id, { type: "CREDIT", amount: 25 }); // top up (DEBIT to spend)
142
+
143
+ // billing config
144
+ await cnk.customers.setCurrency(c.id, "EUR");
145
+ await cnk.customers.setCreditTypes(c.id, ["cr_1", "cr_2"]);
146
+
147
+ // grant / claw back balances (delta is signed)
148
+ await cnk.customers.adjustCredit(c.id, { creditId: "cr_1", delta: 100 });
149
+ const { remaining } = await cnk.customers.adjustOutcome(c.id, { outcomeId: "out_1", delta: -1 });
150
+ await cnk.customers.adjustUnit(c.id, { unitId: "un_1", delta: 5 });
151
+ ```
152
+
153
+ ## Plans & purchases
154
+
155
+ ```ts
156
+ // define a plan
157
+ const plan = await cnk.plans.create({
158
+ name: "Pro",
159
+ billingCycle: "MONTHLY",
160
+ currencyCode: "USD",
161
+ components: [
162
+ { type: "WALLET", billingMode: "ADVANCE", amount: 100 },
163
+ { type: "CREDIT", billingMode: "ADVANCE", creditId: "cr_1", quantity: 10_000 },
164
+ ],
165
+ });
166
+ await cnk.plans.list({ active: true });
167
+ await cnk.plans.setActive(plan.id, false);
168
+
169
+ // subscribe a customer (auto-cancels their existing active/scheduled purchase)
170
+ const purchase = await cnk.purchases.create({ customerId: c.id, planId: plan.id });
171
+ await cnk.purchases.setAutoPayment(purchase.id, false, { pauseReason: "trial" });
172
+ await cnk.purchases.cancel(purchase.id);
173
+ ```
174
+
175
+ ## Invoices & payments (read-only)
176
+
177
+ ```ts
178
+ const invoices = await cnk.invoices.list({ customerId: c.id, status: "OPEN" });
179
+ const { invoice, plan } = await cnk.invoices.get(invoices[0].id);
180
+ const { url } = await cnk.invoices.payLink(invoices[0].id); // hosted pay link (OPEN only)
181
+
182
+ const payments = await cnk.payments.list({ customerId: c.id });
183
+ await cnk.payments.get(payments[0].id); // id = the paid invoice id
184
+ ```
185
+
186
+ ## Catalogs: credits, outcomes, units
187
+
188
+ Define the billable *types*; record usage against them with `signals.*`.
189
+
190
+ ```ts
191
+ // credit type — signals.credit({ agentKey }) meters against it
192
+ await cnk.credits.create({ name: "API", agentKey: "api_credit", basePrice: 1, marginPercent: 20, pricePerCredit: 0.01 });
193
+ await cnk.credits.list({ active: true });
194
+
195
+ // outcome type — a multi-step workflow; each step has its own agentKey
196
+ await cnk.outcomes.create({
197
+ name: "Resolved ticket",
198
+ basePrice: 1, marginPercent: 20, pricePerOutcome: 2,
199
+ steps: [{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 }],
200
+ });
201
+
202
+ // unit type — a quantity-priced meter (FLAT / SLAB / VOLUME)
203
+ await cnk.units.create({ name: "Seats", pricingType: "FLAT", flatPrice: 5 });
204
+ ```
205
+
206
+ ### Unit metering
207
+
208
+ Record a unit event (no tokens) and read a customer's unit balances:
209
+
210
+ ```ts
211
+ await cnk.signals.unit({ customerId: c.id, unit: "Seats" });
212
+ await cnk.signals.unitUsage({ customerId: c.id });
213
+ ```
214
+
215
+ ## Webhooks
216
+
217
+ Threshold alerts — at most one per kind (`CREDIT` / `OUTCOME` / `WALLET`):
218
+
219
+ ```ts
220
+ await cnk.webhooks.upsert({ kind: "WALLET", threshold: 10, url: "https://acme.com/hooks/clocknext" });
221
+ await cnk.webhooks.list();
222
+ await cnk.webhooks.delete("WALLET");
223
+ ```
224
+
114
225
  ## Customer portal token
115
226
 
116
227
  ```ts
117
228
  const { token, expiresAt } = await cnk.portal.createToken({ customerId: "cus_123", ttlSeconds: 600 });
118
- // render <iframe src={`https://app.clocknext.com/portal/embed?token=${token}`} />
229
+ // render <iframe src={`https://payments.clocknext.com/portal/embed?token=${token}`} />
119
230
  ```
120
231
 
121
232
  ## Errors
@@ -137,14 +248,14 @@ try {
137
248
  `ValidationError (400)` · `AuthError (401)` · `NotFoundError (404)` ·
138
249
  `PlanError (422)` · `AllowanceError (422)` · `ConflictError (409)` ·
139
250
  `RateLimitError (429)` · `ServerError (5xx)` · `NetworkError` — all extend
140
- `ClockNextError` (`.status`, `.retryable`, `.idempotencyKey`).
251
+ `ClockNextError` (`.status`, `.retryable`).
141
252
 
142
253
  ## Configuration
143
254
 
144
255
  ```ts
145
256
  new ClockNext({
146
257
  apiKey: "cnk_…", // required
147
- baseUrl: "https://app.clocknext.com",
258
+ baseUrl: "https://payments.clocknext.com",
148
259
  mode: "async", // "async" | "sync"
149
260
  timeoutMs: 10_000,
150
261
  batch: { maxSize: 20, maxIntervalMs: 2000, maxConcurrency: 5, maxQueueSize: 10_000 },
@@ -156,6 +267,9 @@ new ClockNext({
156
267
  });
157
268
  ```
158
269
 
270
+ Numeric options are validated: a negative or non-finite value (e.g. a bad
271
+ `timeoutMs`) falls back to its default rather than silently breaking the client.
272
+
159
273
  ## License
160
274
 
161
275
  MIT