@clocknext/sdk 0.11.0 → 0.12.1
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 +549 -264
- package/dist/index.cjs +257 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +228 -43
- package/dist/index.d.ts +228 -43
- package/dist/index.js +257 -117
- package/dist/index.js.map +1 -1
- package/examples/node-basic.ts +93 -0
- package/examples/serverless-flush.ts +47 -0
- package/package.json +3 -1
- package/src/client.ts +107 -0
- package/src/config.ts +116 -0
- package/src/errors.ts +215 -0
- package/src/filters.ts +33 -0
- package/src/flusher.ts +144 -0
- package/src/http.ts +178 -0
- package/src/idempotency.ts +33 -0
- package/src/index.ts +165 -0
- package/src/queue.ts +48 -0
- package/src/resources/composites.ts +47 -0
- package/src/resources/credits.ts +76 -0
- package/src/resources/customers.ts +298 -0
- package/src/resources/invoices.ts +51 -0
- package/src/resources/outcomes.ts +82 -0
- package/src/resources/payments.ts +30 -0
- package/src/resources/plans.ts +76 -0
- package/src/resources/portal.ts +31 -0
- package/src/resources/purchases.ts +75 -0
- package/src/resources/signals.ts +393 -0
- package/src/resources/units.ts +76 -0
- package/src/resources/webhooks.ts +37 -0
- package/src/resources/workspace.ts +33 -0
- package/src/retry.ts +56 -0
- package/src/types.ts +1601 -0
package/README.md
CHANGED
|
@@ -1,12 +1,108 @@
|
|
|
1
1
|
# @clocknext/sdk
|
|
2
2
|
|
|
3
|
-
Official
|
|
4
|
-
**plans, purchases, invoices, customers, catalogs, and billing** from your
|
|
5
|
-
backend — full coverage of the ClockNext `/api/v1` surface.
|
|
3
|
+
Official TypeScript/JavaScript SDK for ClockNext. Use it from a backend to:
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
- record credit, wallet, outcome, and unit usage;
|
|
6
|
+
- read customer usage, balances, plans, invoices, and payments;
|
|
7
|
+
- manage customers, billing plans, purchases, catalogs, webhooks, and portal tokens.
|
|
8
|
+
|
|
9
|
+
The SDK targets the ClockNext `/api/v1` API and requires Node.js 18 or newer.
|
|
10
|
+
It has zero runtime dependencies and works with native `fetch` in Node, Bun,
|
|
11
|
+
Deno, and edge runtimes.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Read this before you write any code
|
|
16
|
+
|
|
17
|
+
Two facts decide whether your integration works. Neither is obvious from the
|
|
18
|
+
method signatures, and getting either wrong produces **no error at all**.
|
|
19
|
+
|
|
20
|
+
### 1. A successful signal call does not mean the usage was recorded
|
|
21
|
+
|
|
22
|
+
Every `POST /api/v1/signal/*` route answers `202 Accepted` as soon as it has
|
|
23
|
+
written your request to a durable inbox. It is validated and priced afterwards,
|
|
24
|
+
by a background worker. So this resolves successfully even when the signal is
|
|
25
|
+
about to be thrown away:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const result = await cnk.signals.credit(
|
|
29
|
+
{ customerId, model: model.modelId, agentKey: credit.agentKey, tokens: { input: 1, output: 1 } },
|
|
30
|
+
{ wait: true },
|
|
31
|
+
);
|
|
32
|
+
// { status: "accepted", usageLog: null, rawId: "...", messageId: "..." }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That is a deliberate trade: metering must never put a billing round-trip on your
|
|
36
|
+
request path. But it means **rejections cannot come back through the promise**.
|
|
37
|
+
A signal is silently discarded if any of these is true:
|
|
38
|
+
|
|
39
|
+
| Cause | How to avoid it |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `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()` |
|
|
45
|
+
| `member` is set to an email that is not a member of that customer | `customers.addMember()` first, or omit `member` |
|
|
46
|
+
| a required field is missing | see the tables below |
|
|
47
|
+
| the allowance is exhausted | top up with `customers.adjustCredit()` etc. |
|
|
48
|
+
|
|
49
|
+
Three things follow.
|
|
50
|
+
|
|
51
|
+
**The per-signal verdict lives in the ClockNext dashboard, and only there.**
|
|
52
|
+
Every signal is logged with its outcome — accepted, processed, or failed with a
|
|
53
|
+
reason — on the workspace's signal log. That view is owner-only in the product
|
|
54
|
+
UI by design and is not exposed through this SDK or any API. When usage is not
|
|
55
|
+
being billed and the causes below do not explain it, that log is where you look.
|
|
56
|
+
|
|
57
|
+
**`onError` does not see worker-side rejections.** It fires when the HTTP
|
|
58
|
+
request itself fails — network, timeout, auth, a `4xx` returned *before* the
|
|
59
|
+
`202`. A signal that was accepted and then rejected during pricing never
|
|
60
|
+
reaches it, because from the SDK's point of view that request succeeded. Pass it
|
|
61
|
+
anyway: it is how you catch a bad API key, an unreachable host, and a dropped
|
|
62
|
+
buffer.
|
|
63
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const usage = await cnk.signals.list({ customerId });
|
|
72
|
+
|
|
73
|
+
// Nothing here at all → rejected before pricing. Check the dashboard's log.
|
|
74
|
+
console.log(usage.totals.count);
|
|
75
|
+
|
|
76
|
+
// 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
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Nothing can be metered until five things exist
|
|
83
|
+
|
|
84
|
+
A signal is not self-contained; it references rows that must already be there.
|
|
85
|
+
The full chain, in order:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
enabled model → catalog entitlement (credit / outcome / unit, with an agentKey)
|
|
89
|
+
→ plan containing that entitlement
|
|
90
|
+
→ purchase linking the customer to the plan
|
|
91
|
+
→ customer
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`customerId`, `model` and `agentKey` are **not strings you can invent**. The
|
|
95
|
+
quick start below discovers each one instead of hard-coding it.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Security: backend only
|
|
100
|
+
|
|
101
|
+
`cnk_...` API keys have organization-wide access. Never expose one in browser
|
|
102
|
+
code, mobile apps, frontend bundles, or public Git repositories.
|
|
103
|
+
|
|
104
|
+
Use `cnk.portal.createToken()` on your backend when a browser needs to embed the
|
|
105
|
+
customer portal. The browser should receive only the short-lived portal token.
|
|
10
106
|
|
|
11
107
|
## Install
|
|
12
108
|
|
|
@@ -14,374 +110,563 @@ backend — full coverage of the ClockNext `/api/v1` surface.
|
|
|
14
110
|
npm install @clocknext/sdk
|
|
15
111
|
```
|
|
16
112
|
|
|
17
|
-
|
|
18
|
-
Web-standard `fetch` and `crypto`, so it also runs on edge runtimes (Cloudflare
|
|
19
|
-
Workers, Vercel Edge, Deno, Bun).
|
|
113
|
+
## Quick start — zero to a recorded signal
|
|
20
114
|
|
|
21
|
-
|
|
115
|
+
This is a complete, runnable setup for an empty workspace. Every id is read back
|
|
116
|
+
from the API rather than guessed.
|
|
22
117
|
|
|
23
118
|
```ts
|
|
24
119
|
import { ClockNext } from "@clocknext/sdk";
|
|
25
120
|
|
|
26
|
-
const
|
|
121
|
+
const apiKey = process.env.CLOCKNEXT_API_KEY;
|
|
122
|
+
if (!apiKey) throw new Error("CLOCKNEXT_API_KEY is required");
|
|
123
|
+
|
|
124
|
+
const cnk = new ClockNext({
|
|
125
|
+
apiKey,
|
|
126
|
+
// Catches request-level failures (bad key, network, dropped buffer).
|
|
127
|
+
// Rejections found later, during pricing, are visible only in the dashboard.
|
|
128
|
+
onError: (error, signal) =>
|
|
129
|
+
console.error("[clocknext] signal rejected", signal?.type, error),
|
|
130
|
+
});
|
|
131
|
+
|
|
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
|
+
});
|
|
150
|
+
|
|
151
|
+
// 4. A customer.
|
|
152
|
+
const customer = await cnk.customers.create({
|
|
153
|
+
name: "Acme",
|
|
154
|
+
email: "billing@acme.com",
|
|
155
|
+
});
|
|
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
|
+
await cnk.purchases.create({ customerId: customer.id, planId: plan.id });
|
|
27
168
|
|
|
169
|
+
// 6. Now a signal can land.
|
|
28
170
|
await cnk.signals.credit({
|
|
29
|
-
customerId:
|
|
30
|
-
model:
|
|
31
|
-
agentKey:
|
|
32
|
-
|
|
33
|
-
tokens: { input: 1200, output: 340, cache: 0 },
|
|
171
|
+
customerId: customer.id,
|
|
172
|
+
model: model.modelId,
|
|
173
|
+
agentKey: credit.agentKey,
|
|
174
|
+
tokens: { input: 1200, output: 340 },
|
|
34
175
|
custom: { feature: "summarize" },
|
|
35
176
|
});
|
|
36
177
|
|
|
37
|
-
//
|
|
178
|
+
// 7. Required in async mode before the process can exit.
|
|
38
179
|
await cnk.flush();
|
|
180
|
+
|
|
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");
|
|
39
184
|
```
|
|
40
185
|
|
|
41
|
-
|
|
186
|
+
A runnable version is in [`examples/node-basic.ts`](./examples/node-basic.ts),
|
|
187
|
+
shipped with the package.
|
|
42
188
|
|
|
43
|
-
|
|
44
|
-
a typed helper that enforces the right required fields at compile time, and each
|
|
45
|
-
posts to **its own route** — `/api/v1/signal/credit`, `/signal/wallet`,
|
|
46
|
-
`/signal/outcome`. The signal's `type` is what picks that route; from 0.10 it is
|
|
47
|
-
no longer sent in the body.
|
|
189
|
+
## Signals
|
|
48
190
|
|
|
49
|
-
|
|
50
|
-
> methods, same arguments. What changed is the wire: the SDK now targets the
|
|
51
|
-
> `/api/v1/signal/*` family instead of the deprecated `POST /api/v1/usage`, and
|
|
52
|
-
> the `metric` field is now `composite` (see [Composites](#composites)). Bump the
|
|
53
|
-
> package and you are migrated.
|
|
191
|
+
Signals are billable usage events. Each kind posts to its own route.
|
|
54
192
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
193
|
+
| SDK method | Meter | Required fields |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `signals.credit()` | Credit allowance | `customerId`, `model`, `agentKey`, `tokens` |
|
|
196
|
+
| `signals.wallet()` | Customer wallet | `customerId`, `model`, `tokens` |
|
|
197
|
+
| `signals.outcome()` | Outcome workflow step | `customerId`, `model`, `agentKey`, `runId`, `tokens` |
|
|
198
|
+
| `signals.unit()` | Quantity-based unit | `customerId`, `agentKey` |
|
|
59
199
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit", tokens: { input: 1200, output: 340 } });
|
|
200
|
+
`signals.track(signal)` is the underlying call the four convenience methods wrap;
|
|
201
|
+
use it when the kind is dynamic.
|
|
63
202
|
|
|
64
|
-
|
|
65
|
-
await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
|
|
203
|
+
### Where each value comes from
|
|
66
204
|
|
|
67
|
-
|
|
68
|
-
|
|
205
|
+
| Field | Source | Notes |
|
|
206
|
+
| --- | --- | --- |
|
|
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. |
|
|
211
|
+
| `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. |
|
|
213
|
+
|
|
214
|
+
### Credit and wallet
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
await cnk.signals.credit({
|
|
218
|
+
customerId,
|
|
219
|
+
model: model.modelId,
|
|
220
|
+
agentKey: credit.agentKey,
|
|
221
|
+
tokens: { input: 500, output: 120 },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await cnk.signals.wallet({
|
|
225
|
+
customerId,
|
|
226
|
+
model: model.modelId,
|
|
227
|
+
tokens: { input: 500, output: 120 },
|
|
228
|
+
});
|
|
69
229
|
```
|
|
70
230
|
|
|
231
|
+
Wallet signals take no `agentKey` — they debit money at the model's cost rather
|
|
232
|
+
than drawing on a named entitlement — and cannot be grouped into a composite.
|
|
233
|
+
|
|
71
234
|
### Outcome runs
|
|
72
235
|
|
|
73
|
-
An outcome is billed per
|
|
74
|
-
|
|
75
|
-
step can fire as many times as it needs to. The run bills **once**, when you mark
|
|
76
|
-
the final step `complete: true` — completion is never inferred from step
|
|
77
|
-
coverage. Sending `complete` twice is a safe no-op.
|
|
236
|
+
An outcome is billed once per run, not once per step. Reuse the same `runId` for
|
|
237
|
+
every step and set `complete: true` only on the final one.
|
|
78
238
|
|
|
79
239
|
```ts
|
|
80
240
|
const runId = "ticket_9021";
|
|
81
|
-
|
|
82
|
-
await cnk.signals.outcome({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
241
|
+
|
|
242
|
+
await cnk.signals.outcome({
|
|
243
|
+
customerId,
|
|
244
|
+
model: model.modelId,
|
|
245
|
+
agentKey: "resolved_ticket.triage", // a STEP key
|
|
246
|
+
runId,
|
|
247
|
+
tokens: { input: 600, output: 150 },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await cnk.signals.outcome({
|
|
251
|
+
customerId,
|
|
252
|
+
model: model.modelId,
|
|
253
|
+
agentKey: "resolved_ticket.resolve",
|
|
254
|
+
runId,
|
|
255
|
+
complete: true,
|
|
256
|
+
tokens: { input: 500, output: 200 },
|
|
257
|
+
});
|
|
86
258
|
```
|
|
87
259
|
|
|
88
|
-
|
|
260
|
+
A step may repeat within a run — a verify loop firing three times is three
|
|
261
|
+
signals and one outcome. `complete: true` twice is a safe no-op.
|
|
89
262
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
correlation id for that occurrence (a session, a call, a job):
|
|
263
|
+
### Unit usage
|
|
264
|
+
|
|
265
|
+
Units carry no model and no tokens. `quantity` defaults to `1`.
|
|
94
266
|
|
|
95
267
|
```ts
|
|
96
|
-
await cnk.signals.
|
|
97
|
-
customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit",
|
|
98
|
-
tokens: { input: 1200, output: 340 },
|
|
99
|
-
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
100
|
-
});
|
|
268
|
+
await cnk.signals.unit({ customerId, agentKey: "seats", quantity: 3 });
|
|
101
269
|
```
|
|
102
270
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
271
|
+
`signals.unit()` takes **no `{ wait: true }` option** — unit reports are never
|
|
272
|
+
buffered and are always sent immediately.
|
|
273
|
+
|
|
274
|
+
### Composites
|
|
275
|
+
|
|
276
|
+
A composite groups credit, outcome, and unit signals into one billable
|
|
277
|
+
occurrence. Tag each signal, then close the occurrence explicitly.
|
|
106
278
|
|
|
107
279
|
```ts
|
|
108
|
-
|
|
280
|
+
const composite = { ref: "voice_ai", value: "call_5f3a" };
|
|
281
|
+
|
|
282
|
+
await cnk.signals.credit({
|
|
109
283
|
customerId,
|
|
110
|
-
|
|
284
|
+
model: model.modelId,
|
|
285
|
+
agentKey: credit.agentKey,
|
|
286
|
+
tokens: { input: 100, output: 50 },
|
|
287
|
+
composite,
|
|
111
288
|
});
|
|
289
|
+
|
|
290
|
+
// Closing is ALWAYS explicit. Put it on the line before your own return.
|
|
291
|
+
await cnk.signals.completeComposite({ customerId, composite });
|
|
112
292
|
```
|
|
113
293
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
294
|
+
**An outcome's `complete: true` does not close a composite occurrence.** It
|
|
295
|
+
closes the run, and nothing else — whatever mix of credits, outcomes and units
|
|
296
|
+
the composite wraps. If you tag outcome signals with a `composite`, you must
|
|
297
|
+
still call `completeComposite()`, or that occurrence stays open and is never
|
|
298
|
+
billed.
|
|
118
299
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
own, so this is theirs.
|
|
300
|
+
Closing twice is safe. Closing early is safe — the server parks the ping until
|
|
301
|
+
the first tagged signal arrives. Wallet signals do not participate in composites.
|
|
122
302
|
|
|
123
|
-
|
|
124
|
-
closing *early* — before every entitlement in the bundle has reported, or while
|
|
125
|
-
a prepaid slot pool is full: the server parks the request and applies it when
|
|
126
|
-
the blocker clears, rather than failing it.
|
|
303
|
+
## Delivery status: `buffered`, `accepted`, `recorded`, `dropped`
|
|
127
304
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
305
|
+
Every metering call resolves with a `status` saying how far the signal actually
|
|
306
|
+
got. Branch on it rather than on the legacy `queued` boolean, which collapses
|
|
307
|
+
the first two states into one.
|
|
131
308
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
309
|
+
| `status` | Meaning | Can it still be lost? |
|
|
310
|
+
| --- | --- | --- |
|
|
311
|
+
| `"buffered"` | In this process's memory, not yet sent (async mode) | **Yes** — flush before the process exits |
|
|
312
|
+
| `"accepted"` | Written to the server's durable inbox (`202`), not yet priced | No, but not billed yet either |
|
|
313
|
+
| `"recorded"` | Priced inline; `usageLog` is populated | No |
|
|
314
|
+
| `"dropped"` | The SDK discarded it — client closed, or local queue full | Already lost; `onDrop` fired |
|
|
136
315
|
|
|
137
|
-
|
|
316
|
+
The default mode is `async`: the signal is buffered locally and the call returns
|
|
317
|
+
immediately.
|
|
138
318
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
319
|
+
```ts
|
|
320
|
+
const signal = {
|
|
321
|
+
customerId,
|
|
322
|
+
model: model.modelId,
|
|
323
|
+
agentKey: credit.agentKey,
|
|
324
|
+
tokens: { input: 1200, output: 340 },
|
|
325
|
+
};
|
|
143
326
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
> no single bulk-ingest call, so size your request budget per signal.
|
|
327
|
+
const cnk = new ClockNext({ apiKey }); // async (default)
|
|
328
|
+
await cnk.signals.credit(signal); // -> { status: "buffered" }
|
|
329
|
+
await cnk.signals.credit(signal, { wait: true }); // -> "accepted" | "recorded"
|
|
148
330
|
|
|
149
|
-
|
|
331
|
+
const sync = new ClockNext({ apiKey, mode: "sync" }); // every signal awaits
|
|
332
|
+
```
|
|
150
333
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
334
|
+
`{ wait: true }` and `mode: "sync"` wait for the server to **accept** the
|
|
335
|
+
signal, never for it to finish pricing. No mode of this SDK blocks a request
|
|
336
|
+
thread on a billing result.
|
|
154
337
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
338
|
+
`cnk.pending` reports how many signals are currently buffered.
|
|
339
|
+
|
|
340
|
+
### Serverless and shutdown
|
|
158
341
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
> `{ queued: true, messageId, usageLog: null }` — accepted, not yet recorded, and
|
|
162
|
-
> validated only later (a bad `agentKey` is dropped there, silently). Only when
|
|
163
|
-
> the server prices inline do you get `{ queued: false, usageLog }`. Branch on
|
|
164
|
-
> `queued`; never assume `usageLog` is populated.
|
|
165
|
-
>
|
|
166
|
-
> To confirm a signal landed, read it back with `signals.list({ customerId })`.
|
|
167
|
-
> There is no way to price a signal without recording it — send one against a
|
|
168
|
-
> **sandbox** organisation instead.
|
|
169
|
-
|
|
170
|
-
> In `async` mode, server-side rejections (e.g. insufficient allowance, `422`)
|
|
171
|
-
> surface via the `onError` hook or `flush()`, not from the `track()` call.
|
|
172
|
-
|
|
173
|
-
### Reliability
|
|
174
|
-
|
|
175
|
-
- **Retries:** transient failures (network, timeout, `408/409/429/5xx`) are
|
|
176
|
-
retried with exponential backoff + equal jitter, honoring `Retry-After`.
|
|
177
|
-
Deterministic `4xx` (`400/401/404/422`) are never retried.
|
|
178
|
-
- **Replay safety:** reads (`GET`) and `DELETE` are retried automatically. Metering
|
|
179
|
-
signals (`signals.*`) opt into retries too — made safe by the idempotency key
|
|
180
|
-
below. Other non-idempotent writes — e.g. `customers.create`, `plans.create`,
|
|
181
|
-
`purchases.create` — are *not* auto-retried, so a transient blip can never
|
|
182
|
-
create a duplicate.
|
|
183
|
-
- **Idempotency:** every metering signal carries an idempotency key — auto-
|
|
184
|
-
generated per signal, or pass your own via `idempotencyKey` on the signal —
|
|
185
|
-
reused across all retries, so a retried send is **deduplicated by the server**
|
|
186
|
-
instead of double-counted. Pass a stable key (e.g. your own event id) to also
|
|
187
|
-
dedup across process restarts or at-least-once redelivery.
|
|
188
|
-
- **Flushing:** call `await cnk.flush()` before a serverless return, and
|
|
189
|
-
`await cnk.close()` on graceful shutdown to drain the buffer.
|
|
190
|
-
- **Durability:** the async buffer is **in-memory**. A signal already accepted
|
|
191
|
-
by the server is durable on the server side, but signals still buffered when a
|
|
192
|
-
process crashes (before send) are lost. For zero-loss-before-receipt, either
|
|
193
|
-
run in `sync` mode (or `{ wait: true }`) so the send completes before your
|
|
194
|
-
code continues, or call `flush()`/`close()` at the right boundaries. A
|
|
195
|
-
pluggable before-send persistence store is not yet available.
|
|
196
|
-
|
|
197
|
-
## Customers
|
|
342
|
+
The local queue is in memory, so it must be drained before the process can
|
|
343
|
+
freeze or exit.
|
|
198
344
|
|
|
199
345
|
```ts
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await cnk.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// insights
|
|
210
|
-
await cnk.customers.usage(c.id, { limit: 50 });
|
|
211
|
-
await cnk.customers.balances(c.id);
|
|
212
|
-
await cnk.customers.plan(c.id);
|
|
213
|
-
await cnk.customers.revenue(c.id, { range: "30d" });
|
|
346
|
+
// Serverless handler — flush, but do NOT close: the container is reused.
|
|
347
|
+
try {
|
|
348
|
+
// application work and signal tracking
|
|
349
|
+
} finally {
|
|
350
|
+
await cnk.flush();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Long-running process, on shutdown only:
|
|
354
|
+
await cnk.close();
|
|
214
355
|
```
|
|
215
356
|
|
|
216
|
-
|
|
357
|
+
Construct the client **once at module scope**, not per request. After `close()`
|
|
358
|
+
the client is permanently shut: further signals resolve `{ status: "dropped" }`
|
|
359
|
+
and fire `onDrop` with reason `"closed"`. Never call `close()` in a serverless
|
|
360
|
+
handler.
|
|
361
|
+
|
|
362
|
+
If losing a buffered signal is unacceptable, use `mode: "sync"` or
|
|
363
|
+
`{ wait: true }`, and supply your own stable `idempotencyKey`.
|
|
364
|
+
|
|
365
|
+
## Idempotency and retries
|
|
366
|
+
|
|
367
|
+
Metering calls get an idempotency key automatically and reuse it across every
|
|
368
|
+
retry, so a retried send is deduplicated server-side instead of double-billing.
|
|
369
|
+
Supply your own stable key to also deduplicate across process restarts and
|
|
370
|
+
at-least-once redelivery:
|
|
217
371
|
|
|
218
372
|
```ts
|
|
219
|
-
await cnk.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
373
|
+
await cnk.signals.credit({
|
|
374
|
+
customerId,
|
|
375
|
+
model: model.modelId,
|
|
376
|
+
agentKey: credit.agentKey,
|
|
377
|
+
tokens: { input: 10, output: 5 },
|
|
378
|
+
idempotencyKey: `llm-request:${requestId}`,
|
|
379
|
+
});
|
|
223
380
|
```
|
|
224
381
|
|
|
225
|
-
|
|
382
|
+
When a signal fails permanently, the key it was sent with is available on both
|
|
383
|
+
the error (`error.idempotencyKey`) and the signal handed to `onError`/`onDrop`.
|
|
384
|
+
**Replay with that key** — generating a new one bills the event twice.
|
|
226
385
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
386
|
+
Retries apply to network failures, timeouts, `408`, `429`, `5xx`, and the one
|
|
387
|
+
transient `409`. Validation, authentication, not-found, plan, allowance and
|
|
388
|
+
permanent-conflict errors are never retried. Reads and `DELETE`s are retried;
|
|
389
|
+
`POST`/`PATCH` are not, except metering calls, which opt in because they are
|
|
390
|
+
idempotency-keyed.
|
|
231
391
|
|
|
232
|
-
|
|
233
|
-
|
|
392
|
+
Client-side batching is timing and concurrency management, not a bulk API — each
|
|
393
|
+
signal is still an individual HTTP request.
|
|
234
394
|
|
|
235
|
-
|
|
236
|
-
await cnk.customers.adjustCredit(c.id, { creditId: "cr_1", delta: 100 });
|
|
237
|
-
const { remaining } = await cnk.customers.adjustOutcome(c.id, { outcomeId: "out_1", delta: -1 });
|
|
238
|
-
await cnk.customers.adjustUnit(c.id, { unitId: "un_1", delta: 5 });
|
|
239
|
-
```
|
|
395
|
+
## Handling errors
|
|
240
396
|
|
|
241
|
-
|
|
397
|
+
Branch on error classes, never on message text.
|
|
242
398
|
|
|
243
399
|
```ts
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
400
|
+
import {
|
|
401
|
+
ClockNextError,
|
|
402
|
+
AllowanceError,
|
|
403
|
+
AuthError,
|
|
404
|
+
ConflictError,
|
|
405
|
+
ForbiddenError,
|
|
406
|
+
NetworkError,
|
|
407
|
+
NotFoundError,
|
|
408
|
+
PlanError,
|
|
409
|
+
RateLimitError,
|
|
410
|
+
ServerError,
|
|
411
|
+
ValidationError,
|
|
412
|
+
} from "@clocknext/sdk";
|
|
256
413
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
414
|
+
try {
|
|
415
|
+
await cnk.customers.get(customerId);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (error instanceof NotFoundError) {
|
|
418
|
+
// No such customer in this workspace.
|
|
419
|
+
} else if (error instanceof AuthError) {
|
|
420
|
+
// Rotate or correct the API key.
|
|
421
|
+
} else if (error instanceof ValidationError) {
|
|
422
|
+
// Fix the request shape or required fields.
|
|
423
|
+
} else if (error instanceof RateLimitError || error instanceof ServerError) {
|
|
424
|
+
// Already retried by the SDK; add an application-level fallback.
|
|
425
|
+
} else if (error instanceof ClockNextError) {
|
|
426
|
+
console.error(error.status, error.code, error.message);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
261
429
|
```
|
|
262
430
|
|
|
263
|
-
|
|
431
|
+
| Class | Status | Meaning |
|
|
432
|
+
| --- | --- | --- |
|
|
433
|
+
| `ValidationError` | 400 | Malformed request or failed schema validation |
|
|
434
|
+
| `AuthError` | 401 | Missing, invalid or expired API key |
|
|
435
|
+
| `ForbiddenError` | 403 | Key is valid but not permitted to do this |
|
|
436
|
+
| `NotFoundError` | 404 | Unknown resource (also returned for cross-org access) |
|
|
437
|
+
| `ConflictError` | 409 | Conflict — usually an archived customer. Permanent, and not retried |
|
|
438
|
+
| `PlanError` | 422 | No active plan, meter not in plan, model not enabled, member missing |
|
|
439
|
+
| `AllowanceError` | 422 | The entitlement ran out — top up or upsell |
|
|
440
|
+
| `RateLimitError` | 429 | Rate limited; carries `retryAfterMs` |
|
|
441
|
+
| `ServerError` | 5xx | Server-side failure |
|
|
442
|
+
| `NetworkError` | — | fetch threw or the request timed out |
|
|
443
|
+
|
|
444
|
+
All extend `ClockNextError`, which exposes `.status` (`number | undefined`),
|
|
445
|
+
`.retryable`, `.code` (the server's machine-readable code, when it sends one),
|
|
446
|
+
and `.idempotencyKey` on metering calls. Constructor failures — a missing
|
|
447
|
+
`apiKey`, no available `fetch` — throw `ValidationError`, so a single
|
|
448
|
+
`instanceof ClockNextError` catches setup problems too.
|
|
449
|
+
|
|
450
|
+
### Signal failures do not arrive here
|
|
451
|
+
|
|
452
|
+
Because ingest answers `202` before validating, **a try/catch around
|
|
453
|
+
`signals.credit()` will not catch a rejected signal**, with or without
|
|
454
|
+
`{ wait: true }` — and neither will `onError`. A rejection discovered during
|
|
455
|
+
pricing has no programmatic channel at all; it is recorded on the workspace's
|
|
456
|
+
signal log in the ClockNext dashboard, which is owner-only by design.
|
|
457
|
+
|
|
458
|
+
What the hooks below DO cover is everything that fails before the `202`: an
|
|
459
|
+
invalid API key, an unreachable host, a timeout, and signals the SDK dropped
|
|
460
|
+
without sending.
|
|
264
461
|
|
|
265
462
|
```ts
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
463
|
+
import { ClockNext, ClockNextError } from "@clocknext/sdk";
|
|
464
|
+
|
|
465
|
+
const cnk = new ClockNext({
|
|
466
|
+
apiKey,
|
|
467
|
+
onError: (error, signal) => {
|
|
468
|
+
// `error` is `unknown`; narrow it.
|
|
469
|
+
if (error instanceof ClockNextError) {
|
|
470
|
+
logger.error({ status: error.status, key: error.idempotencyKey }, error.message);
|
|
471
|
+
}
|
|
472
|
+
deadLetter.push(signal); // replay later with signal.idempotencyKey
|
|
473
|
+
},
|
|
474
|
+
onDrop: (signal, reason) => logger.error({ signal, reason }, "signal dropped"),
|
|
475
|
+
onRetry: ({ attempt, delayMs, error }) => logger.warn({ attempt, delayMs, error }),
|
|
476
|
+
onFlush: (sentCount) => logger.info({ sentCount }, "signals flushed"),
|
|
477
|
+
});
|
|
272
478
|
```
|
|
273
479
|
|
|
274
|
-
|
|
480
|
+
`onFlush` counts signals the server **accepted**, not signals it billed.
|
|
275
481
|
|
|
276
|
-
|
|
482
|
+
So the division of labour is: these hooks tell you your integration cannot
|
|
483
|
+
reach ClockNext; `signals.list()` tells you what got priced; the dashboard's
|
|
484
|
+
signal log tells you why an individual signal did not.
|
|
277
485
|
|
|
278
|
-
|
|
279
|
-
// credit type — signals.credit({ agentKey }) meters against it
|
|
280
|
-
await cnk.credits.create({ name: "API", agentKey: "api_credit", basePrice: 1, marginPercent: 20, pricePerCredit: 0.01 });
|
|
281
|
-
await cnk.credits.list({ active: true });
|
|
282
|
-
|
|
283
|
-
// outcome type — a multi-step workflow. TWO kinds of agentKey, both required and
|
|
284
|
-
// both unique per org: the outcome's own (its identity) and each step's (what
|
|
285
|
-
// signals.outcome() reports against). They are separate namespaces.
|
|
286
|
-
await cnk.outcomes.create({
|
|
287
|
-
name: "Resolved ticket",
|
|
288
|
-
agentKey: "resolved_ticket",
|
|
289
|
-
basePrice: 1, marginPercent: 20, pricePerOutcome: 2,
|
|
290
|
-
steps: [{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 }],
|
|
291
|
-
});
|
|
486
|
+
## Balances and usage
|
|
292
487
|
|
|
293
|
-
|
|
294
|
-
await cnk.
|
|
488
|
+
```ts
|
|
489
|
+
const balances = await cnk.customers.balances(customerId);
|
|
490
|
+
balances.wallet?.balanceUsd;
|
|
491
|
+
balances.credits; // granted, used, remaining
|
|
492
|
+
balances.outcomes; // granted, remaining, steps
|
|
493
|
+
balances.units; // granted, remaining
|
|
494
|
+
|
|
495
|
+
// Convenience lookups — these fetch the full snapshot, so prefer `balances()`
|
|
496
|
+
// when you need more than one row from the same customer.
|
|
497
|
+
await cnk.customers.creditBalance(customerId, { creditName: "API credit" });
|
|
498
|
+
await cnk.customers.outcomeBalance(customerId, { outcomeName: "Resolved ticket" });
|
|
499
|
+
await cnk.customers.unitBalance(customerId, { unitName: "Seats" });
|
|
500
|
+
|
|
501
|
+
// Usage history, and unit balances + recent unit events.
|
|
502
|
+
await cnk.signals.list({ customerId, limit: 50 });
|
|
503
|
+
await cnk.signals.unitUsage({ customerId, limit: 50 });
|
|
295
504
|
```
|
|
296
505
|
|
|
297
|
-
|
|
506
|
+
Adjustments use stable agent keys, not display names:
|
|
298
507
|
|
|
299
|
-
|
|
508
|
+
```ts
|
|
509
|
+
await cnk.customers.adjustCredit(customerId, { agentKey: "api_credit", delta: 100 });
|
|
510
|
+
await cnk.customers.adjustOutcome(customerId, { agentKey: "resolved_ticket.triage", delta: 1 });
|
|
511
|
+
await cnk.customers.adjustUnit(customerId, { agentKey: "seats", delta: 5 });
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Customers, plans, and billing
|
|
300
515
|
|
|
301
516
|
```ts
|
|
302
|
-
await cnk.
|
|
517
|
+
const customer = await cnk.customers.create({ name: "Acme", email: "billing@acme.com" });
|
|
518
|
+
await cnk.customers.update(customer.id, { currencyCode: "USD" });
|
|
519
|
+
await cnk.customers.list({ limit: 50, q: "acme" });
|
|
303
520
|
|
|
304
|
-
|
|
305
|
-
|
|
521
|
+
for await (const c of cnk.customers.iterate()) {
|
|
522
|
+
// follows the cursor for you
|
|
523
|
+
}
|
|
306
524
|
|
|
307
|
-
|
|
308
|
-
await cnk.
|
|
309
|
-
|
|
310
|
-
composite: { ref: "voice_ai", value: "call_5f3a" },
|
|
311
|
-
idempotencyKey: `seats:${requestId}`,
|
|
525
|
+
const purchase = await cnk.purchases.create({ customerId: customer.id, planId: plan.id });
|
|
526
|
+
await cnk.purchases.setAutoPayment(purchase.id, false, {
|
|
527
|
+
pauseReason: "Customer requested manual payment",
|
|
312
528
|
});
|
|
313
|
-
|
|
314
|
-
await cnk.signals.unitUsage({ customerId: c.id });
|
|
315
529
|
```
|
|
316
530
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
531
|
+
Additional resource methods:
|
|
532
|
+
|
|
533
|
+
```text
|
|
534
|
+
cnk.customers.get / update / delete / iterate
|
|
535
|
+
cnk.customers.listMembers / addMember / updateMember / removeMember
|
|
536
|
+
cnk.customers.wallet / addWalletEntry / usage / plan / balances
|
|
537
|
+
|
|
538
|
+
cnk.plans.list / get / create / update / setActive / delete
|
|
539
|
+
cnk.purchases.list / get / create / cancel / setAutoPayment
|
|
540
|
+
cnk.invoices.list / get / payLink
|
|
541
|
+
cnk.payments.list / get
|
|
542
|
+
cnk.webhooks.list / upsert / delete
|
|
543
|
+
cnk.portal.createToken
|
|
544
|
+
```
|
|
320
545
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
> (on the old route a retried report drew the balance down twice). Because it can
|
|
325
|
-
> now answer `202`, this resolves `{ queued, unitUsage, rawId }` rather than a
|
|
326
|
-
> bare `{ unitUsage }` — branch on `queued`, don't assume `unitUsage`.
|
|
546
|
+
Note: `plans.update()` is a full rewrite and takes the *input* shape, which is
|
|
547
|
+
not the shape `plans.get()` returns. Rebuild the components rather than passing
|
|
548
|
+
the read result straight back.
|
|
327
549
|
|
|
328
|
-
##
|
|
550
|
+
## Catalogs and workspace discovery
|
|
329
551
|
|
|
330
|
-
|
|
552
|
+
This is where `agentKey` and `modelId` come from.
|
|
331
553
|
|
|
332
554
|
```ts
|
|
333
|
-
await cnk.
|
|
334
|
-
await cnk.
|
|
335
|
-
await cnk.
|
|
555
|
+
const models = await cnk.workspace.models({ active: true });
|
|
556
|
+
const credits = await cnk.credits.list({ active: true });
|
|
557
|
+
const outcomes = await cnk.outcomes.list({ active: true });
|
|
558
|
+
const units = await cnk.units.list({ active: true });
|
|
559
|
+
const composites = await cnk.composites.list({ active: true });
|
|
336
560
|
```
|
|
337
561
|
|
|
338
|
-
|
|
562
|
+
`{ active: true }` returns only active rows, `{ active: false }` only archived
|
|
563
|
+
ones, and omitting it returns both.
|
|
564
|
+
|
|
565
|
+
Creating catalog entries:
|
|
339
566
|
|
|
340
567
|
```ts
|
|
341
|
-
const
|
|
342
|
-
|
|
568
|
+
const credit = await cnk.credits.create({
|
|
569
|
+
name: "API credit",
|
|
570
|
+
agentKey: "api_credit", // stable identity; a rename never changes it
|
|
571
|
+
basePrice: 10,
|
|
572
|
+
marginPercent: 20,
|
|
573
|
+
pricePerCredit: 12, // recomputed server-side from basePrice × margin
|
|
574
|
+
tokensPerCredit: 1000,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const outcome = await cnk.outcomes.create({
|
|
578
|
+
name: "Resolved ticket",
|
|
579
|
+
agentKey: "resolved_ticket", // the OUTCOME's key
|
|
580
|
+
basePrice: 1,
|
|
581
|
+
marginPercent: 20,
|
|
582
|
+
pricePerOutcome: 1.2,
|
|
583
|
+
steps: [
|
|
584
|
+
{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 },
|
|
585
|
+
{ name: "Resolve", agentKey: "resolved_ticket.resolve", basePrice: 0.5 },
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const unit = await cnk.units.create({
|
|
590
|
+
name: "Seats",
|
|
591
|
+
agentKey: "seats",
|
|
592
|
+
pricingType: "FLAT",
|
|
593
|
+
flatPrice: 5, // price per event; tiered units take `tiers` instead
|
|
594
|
+
});
|
|
343
595
|
```
|
|
344
596
|
|
|
345
|
-
|
|
597
|
+
Signals report against a **step's** `agentKey`, never the outcome's own.
|
|
598
|
+
|
|
599
|
+
Other catalog methods:
|
|
600
|
+
|
|
601
|
+
```text
|
|
602
|
+
cnk.credits.create / get / update / setActive / delete
|
|
603
|
+
cnk.outcomes.create / get / update / setActive / delete
|
|
604
|
+
cnk.units.create / get / update / setActive / delete
|
|
605
|
+
cnk.composites.list / create
|
|
606
|
+
```
|
|
346
607
|
|
|
347
|
-
|
|
608
|
+
## Webhooks and customer portal
|
|
348
609
|
|
|
349
610
|
```ts
|
|
350
|
-
|
|
611
|
+
await cnk.webhooks.upsert({
|
|
612
|
+
kind: "WALLET", // "WALLET" | "CREDIT" | "OUTCOME"
|
|
613
|
+
threshold: 10,
|
|
614
|
+
url: "https://example.com/clocknext/webhook",
|
|
615
|
+
});
|
|
616
|
+
await cnk.webhooks.list();
|
|
617
|
+
await cnk.webhooks.delete("WALLET");
|
|
351
618
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
else if (err instanceof AuthError) {/* bad key */}
|
|
357
|
-
else if (err instanceof RateLimitError) {/* back off */}
|
|
358
|
-
}
|
|
619
|
+
const { token, expiresAt } = await cnk.portal.createToken({
|
|
620
|
+
customerId,
|
|
621
|
+
ttlSeconds: 600,
|
|
622
|
+
});
|
|
359
623
|
```
|
|
360
624
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
`RateLimitError (429)` · `ServerError (5xx)` · `NetworkError` — all extend
|
|
364
|
-
`ClockNextError` (`.status`, `.retryable`).
|
|
625
|
+
Pass the portal token to your frontend and embed it using your portal URL. Never
|
|
626
|
+
pass the API key to the frontend.
|
|
365
627
|
|
|
366
628
|
## Configuration
|
|
367
629
|
|
|
368
630
|
```ts
|
|
369
|
-
new ClockNext({
|
|
370
|
-
apiKey
|
|
371
|
-
baseUrl: "https://payments.clocknext.com",
|
|
372
|
-
mode: "async",
|
|
631
|
+
const cnk = new ClockNext({
|
|
632
|
+
apiKey,
|
|
633
|
+
baseUrl: "https://payments.clocknext.com", // optional
|
|
634
|
+
mode: "async", // "async" (default) | "sync"
|
|
373
635
|
timeoutMs: 10_000,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
636
|
+
fetch: myFetch, // optional fetch implementation
|
|
637
|
+
logger: { warn: console.warn, debug: console.debug },
|
|
638
|
+
batch: {
|
|
639
|
+
maxSize: 20,
|
|
640
|
+
maxIntervalMs: 2_000,
|
|
641
|
+
maxConcurrency: 5,
|
|
642
|
+
maxQueueSize: 10_000,
|
|
643
|
+
},
|
|
644
|
+
retry: {
|
|
645
|
+
maxAttempts: 5,
|
|
646
|
+
baseDelayMs: 200,
|
|
647
|
+
maxDelayMs: 10_000,
|
|
648
|
+
},
|
|
649
|
+
onError: (error, signal) => {},
|
|
379
650
|
onDrop: (signal, reason) => {},
|
|
651
|
+
onRetry: ({ attempt, delayMs, error }) => {},
|
|
652
|
+
onFlush: (sentCount) => {},
|
|
380
653
|
});
|
|
381
654
|
```
|
|
382
655
|
|
|
383
|
-
|
|
384
|
-
|
|
656
|
+
With no `onError` supplied the SDK reports failures on `console.error` rather
|
|
657
|
+
than swallowing them. Pass `onError: () => {}` to silence that deliberately.
|
|
658
|
+
|
|
659
|
+
## Testing and packaging
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
npm run typecheck
|
|
663
|
+
npm test
|
|
664
|
+
npm run build
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
The package publishes ESM and CommonJS builds with TypeScript declarations, and
|
|
668
|
+
ships its annotated `src/` so source maps resolve and the implementation is
|
|
669
|
+
readable from `node_modules`.
|
|
385
670
|
|
|
386
671
|
## License
|
|
387
672
|
|