@clocknext/sdk 0.11.0 → 0.12.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 +518 -264
- package/dist/index.cjs +257 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +219 -43
- package/dist/index.d.ts +219 -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 +112 -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 +386 -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 +1599 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the ClockNext SDK.
|
|
3
|
+
*
|
|
4
|
+
* The signal types mirror the bodies of the `/api/v1/signal/*` routes as a
|
|
5
|
+
* discriminated union, so the per-kind required fields the server enforces with
|
|
6
|
+
* Zod are enforced here at compile time instead of surfacing as runtime 400s.
|
|
7
|
+
*
|
|
8
|
+
* The server now has one schema per route rather than one schema with a `type`
|
|
9
|
+
* discriminator, which is why `type` no longer appears on the wire — it selects
|
|
10
|
+
* the route instead. It stays on these types because it is still the
|
|
11
|
+
* discriminator callers write and TypeScript narrows on.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Signals
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type SignalType = "wallet" | "credit" | "outcome";
|
|
19
|
+
export type SignalStatus = "SUCCESS" | "FAILURE" | "PENDING";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Token counts for a signal. `input` and `output` are required — every metered
|
|
23
|
+
* call has them, and the server requires them. `cache` (provider-specific
|
|
24
|
+
* prompt-cache reads) is optional and defaults to 0 server-side when omitted.
|
|
25
|
+
*/
|
|
26
|
+
export interface Tokens {
|
|
27
|
+
input: number;
|
|
28
|
+
output: number;
|
|
29
|
+
cache?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Groups a signal into one occurrence of a **composite** — a bundle of credits
|
|
34
|
+
* / outcomes / units that is priced and billed as a single thing.
|
|
35
|
+
*
|
|
36
|
+
* `ref` is the composite's `refId` (defined in the dashboard) and `value` is
|
|
37
|
+
* your own correlation id for one occurrence: a session, a call, a job. Every
|
|
38
|
+
* signal sharing a value belongs to the same occurrence.
|
|
39
|
+
*
|
|
40
|
+
* This is NOT reporting-only (earlier versions of this SDK called it a
|
|
41
|
+
* "pricing metric" and said so — that was wrong). Where the customer's plan
|
|
42
|
+
* sells the composite, the occurrence IS the billable unit: it opens on the
|
|
43
|
+
* first tagged signal costing nothing, and is charged once when you close it
|
|
44
|
+
* with `signals.completeComposite()` — claiming a prepaid slot on an ADVANCE
|
|
45
|
+
* component, or becoming countable on an ARREAR one.
|
|
46
|
+
*
|
|
47
|
+
* What stays true is that a bad tag never fails the signal: a `ref` naming no
|
|
48
|
+
* live composite is ignored rather than rejected, because the signal carries
|
|
49
|
+
* money and the tag only decides what it is grouped under. Applies to credit,
|
|
50
|
+
* outcome and unit signals; a tag on a wallet signal is accepted and ignored,
|
|
51
|
+
* since raw wallet spend is not entitlement traffic.
|
|
52
|
+
*
|
|
53
|
+
* Only ONE composite per signal is expressible here. The wire format accepts
|
|
54
|
+
* several (`composite` is an object of ref → value), so if you need a signal
|
|
55
|
+
* in two composites at once, that is the limit to lift.
|
|
56
|
+
*/
|
|
57
|
+
export interface CompositeTag {
|
|
58
|
+
/** The composite's `refId`. Case-insensitive. Unlike earlier versions this
|
|
59
|
+
* no longer shares a namespace with the ingest fields — the tag is nested
|
|
60
|
+
* under `composite` on the wire — so a composite may safely be called
|
|
61
|
+
* `model`, `status`, or anything else. */
|
|
62
|
+
ref: string;
|
|
63
|
+
/** Your correlation value for this occurrence, stored verbatim. */
|
|
64
|
+
value: string | number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Workspace (mirrors GET /api/v1/me and GET /api/v1/models)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** The organisation + key behind a request — `cnk.workspace.me()`. */
|
|
72
|
+
export interface WorkspaceIdentity {
|
|
73
|
+
organization: {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
slug: string | null;
|
|
77
|
+
/** `sandbox` is a disposable staging twin; `live` bills for real. Check
|
|
78
|
+
* this before sending production traffic. */
|
|
79
|
+
environment: "sandbox" | "live";
|
|
80
|
+
};
|
|
81
|
+
apiKey: {
|
|
82
|
+
prefix: string;
|
|
83
|
+
lastFour: string;
|
|
84
|
+
expiresAt: string | null;
|
|
85
|
+
} | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One model the org has enabled — `cnk.workspace.models()`. The `modelId` is
|
|
89
|
+
* what you put in a signal's `model`. Prices are USD per 1,000,000 tokens. */
|
|
90
|
+
export interface Model {
|
|
91
|
+
modelId: string;
|
|
92
|
+
modelName: string;
|
|
93
|
+
provider: string;
|
|
94
|
+
providerName: string;
|
|
95
|
+
isActive: boolean;
|
|
96
|
+
inputPrice: number;
|
|
97
|
+
outputPrice: number;
|
|
98
|
+
cachePrice: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface SignalBase {
|
|
102
|
+
/** The customer this usage belongs to (required). */
|
|
103
|
+
customerId: string;
|
|
104
|
+
/**
|
|
105
|
+
* The model's catalog id — must be enabled in your ClockNext org
|
|
106
|
+
* (Settings → Models). Matched case-insensitively against `OrgModel.modelId`.
|
|
107
|
+
*/
|
|
108
|
+
model: string;
|
|
109
|
+
/** Token counts for this call — required; every metered signal is priced on
|
|
110
|
+
* tokens. (`cache` within is optional; `input`/`output` are not.) */
|
|
111
|
+
tokens: Tokens;
|
|
112
|
+
/** Optional member (email) this usage is attributed to. */
|
|
113
|
+
member?: string;
|
|
114
|
+
/** Free-form metadata. Credit rules can target `custom.<key>` paths. */
|
|
115
|
+
custom?: Record<string, unknown>;
|
|
116
|
+
/**
|
|
117
|
+
* Idempotency key for safe retries. **Auto-generated (UUID v4) if omitted**
|
|
118
|
+
* and reused across every retry of this signal, so a retried send is
|
|
119
|
+
* deduplicated server-side instead of double-recording. Supply your own — a
|
|
120
|
+
* stable id for this logical event — to also dedup across process restarts or
|
|
121
|
+
* at-least-once redelivery, which an in-memory auto key can't survive.
|
|
122
|
+
*/
|
|
123
|
+
idempotencyKey?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Meters a credit, identified by the credit's stable `agentKey`. */
|
|
127
|
+
export interface CreditSignal extends SignalBase {
|
|
128
|
+
type: "credit";
|
|
129
|
+
/** The Credit's stable agent key (assigned when the credit was created). */
|
|
130
|
+
agentKey: string;
|
|
131
|
+
/** Optional composite this signal belongs to. See {@link CompositeTag}. */
|
|
132
|
+
composite?: CompositeTag;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Debits the customer's wallet in USD at the model's cost. */
|
|
136
|
+
export interface WalletSignal extends SignalBase {
|
|
137
|
+
type: "wallet";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Records one step of a multi-call outcome run. The `agentKey` identifies the
|
|
142
|
+
* step (and the outcome); `runId` groups every signal of a single run, so a step
|
|
143
|
+
* may repeat freely. The run bills once, when the caller marks it `complete`.
|
|
144
|
+
*/
|
|
145
|
+
export interface OutcomeSignal extends SignalBase {
|
|
146
|
+
type: "outcome";
|
|
147
|
+
/** The OutcomeStep's stable agent key — identifies the outcome AND the step. */
|
|
148
|
+
agentKey: string;
|
|
149
|
+
/**
|
|
150
|
+
* Correlates every signal of one workflow run. Required. A step may fire more
|
|
151
|
+
* than once within a run (a "verify" loop firing 3× is three signals, one
|
|
152
|
+
* outcome). Opaque to ClockNext — any string you can correlate; unique per org.
|
|
153
|
+
*/
|
|
154
|
+
runId: string;
|
|
155
|
+
/**
|
|
156
|
+
* Set `true` on the LAST step signal to declare the run finished — completion
|
|
157
|
+
* is never inferred from step coverage. Sending it twice is a safe no-op and
|
|
158
|
+
* can't bill again.
|
|
159
|
+
*
|
|
160
|
+
* This closes the RUN only. If the signal also carries a `composite`, that
|
|
161
|
+
* occurrence stays open and unbilled until you call
|
|
162
|
+
* `signals.completeComposite()` — the two completions are separate, and
|
|
163
|
+
* neither implies the other.
|
|
164
|
+
*/
|
|
165
|
+
complete?: boolean;
|
|
166
|
+
/** Optional composite this signal belongs to. See {@link CompositeTag}. */
|
|
167
|
+
composite?: CompositeTag;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type Signal = CreditSignal | WalletSignal | OutcomeSignal;
|
|
171
|
+
|
|
172
|
+
/** Helper input shapes for the per-type convenience methods. */
|
|
173
|
+
export type CreditInput = Omit<CreditSignal, "type">;
|
|
174
|
+
export type WalletInput = Omit<WalletSignal, "type">;
|
|
175
|
+
export type OutcomeInput = Omit<OutcomeSignal, "type">;
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Server response shapes (mirrors src/lib/usage/log.ts)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
export interface SerializedRuleApplication {
|
|
182
|
+
ruleId: string | null;
|
|
183
|
+
ruleName: string;
|
|
184
|
+
creditsApplied: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface SerializedOutcomeAttribution {
|
|
188
|
+
name: string;
|
|
189
|
+
/** The step of the outcome this signal performed. */
|
|
190
|
+
step: string;
|
|
191
|
+
/** The caller's run id — every signal of one run shares it. `null` on logs
|
|
192
|
+
* recorded before the run-based flow. */
|
|
193
|
+
runId: string | null;
|
|
194
|
+
/** True when this signal ended the run (`complete: true`). */
|
|
195
|
+
closedRun: boolean;
|
|
196
|
+
/** True once the run's completion was counted against the allowance. Diverges
|
|
197
|
+
* from `closedRun` only when a run closes with no allowance left — it waits,
|
|
198
|
+
* then flips true once a top-up settles it. */
|
|
199
|
+
completed: boolean;
|
|
200
|
+
/** How many signals the run holds after this one. */
|
|
201
|
+
signalCount: number;
|
|
202
|
+
/** The outcome's catalogue step count — not a progress target, since steps
|
|
203
|
+
* may repeat within a run. */
|
|
204
|
+
totalSteps: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The customer's remaining balance for the dimension a signal metered, AFTER
|
|
208
|
+
* the signal was applied — update an on-screen meter straight from the record
|
|
209
|
+
* response. Populated on recorded (inline / `{ wait: true }`) signals; `null`
|
|
210
|
+
* on read-back. */
|
|
211
|
+
export interface UsageBalance {
|
|
212
|
+
dimension: "credit" | "outcome" | "wallet";
|
|
213
|
+
/** Post-signal remaining. `null` when the meter is uncapped — an ARREAR
|
|
214
|
+
* (pay-as-you-go) credit or outcome has no fixed allowance. */
|
|
215
|
+
remaining: number | null;
|
|
216
|
+
/** The credit / outcome name; `null` for the wallet. */
|
|
217
|
+
name: string | null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface SerializedUsageLog {
|
|
221
|
+
id: string;
|
|
222
|
+
customerId: string;
|
|
223
|
+
modelName: string | null;
|
|
224
|
+
creditName: string | null;
|
|
225
|
+
member: string | null;
|
|
226
|
+
inputTokens: number;
|
|
227
|
+
outputTokens: number;
|
|
228
|
+
cacheTokens: number;
|
|
229
|
+
/** Total credits deducted: the call's real per-model provider cost as a
|
|
230
|
+
* fraction of the credit's base price (i.e. cost + your margin), plus any
|
|
231
|
+
* credit-rule add-ons; 0 for wallet-money calls. */
|
|
232
|
+
credit: number;
|
|
233
|
+
providedCost: number;
|
|
234
|
+
customerCost: number;
|
|
235
|
+
status: SignalStatus;
|
|
236
|
+
appliedRules: SerializedRuleApplication[];
|
|
237
|
+
outcome: SerializedOutcomeAttribution | null;
|
|
238
|
+
/** Post-signal balance for the metered dimension. Null on
|
|
239
|
+
* read-back. */
|
|
240
|
+
balance: UsageBalance | null;
|
|
241
|
+
createdAt: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Result of a single signal send (sync mode, or `{ wait: true }`).
|
|
246
|
+
*
|
|
247
|
+
* `queued: true` has TWO causes, and they are not the same thing:
|
|
248
|
+
* 1. **Client-side buffering** (async mode, no `wait`) — the signal is in this
|
|
249
|
+
* process's buffer and has not been sent. No `messageId`.
|
|
250
|
+
* 2. **Server-side durable hand-off** — the request WAS sent and the server
|
|
251
|
+
* answered `202 Accepted`, having enqueued it for a worker to price. Carries
|
|
252
|
+
* a `messageId`. `usageLog` is null: the log does not exist yet.
|
|
253
|
+
*
|
|
254
|
+
* In case 2 the signal is accepted but **not yet billed**, and it has only been
|
|
255
|
+
* shallowly validated. A bad `agentKey`, a customer with no matching plan, or an
|
|
256
|
+
* exhausted allowance is detected by the worker LATER and never surfaces here —
|
|
257
|
+
* the signal is simply dropped. A `202` therefore means "accepted", never
|
|
258
|
+
* "recorded". To confirm a signal actually landed, read it back with
|
|
259
|
+
* `signals.list({ customerId })`.
|
|
260
|
+
*
|
|
261
|
+
* When the server has no queue configured it prices inline instead and returns
|
|
262
|
+
* `queued: false` with a populated `usageLog`, validating synchronously.
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* How far a metering call actually got. See {@link TrackResult.status} — the
|
|
267
|
+
* three states differ in whether the signal can still be lost, and in whether
|
|
268
|
+
* it has been billed.
|
|
269
|
+
*/
|
|
270
|
+
export type SignalDelivery = "buffered" | "accepted" | "recorded" | "dropped";
|
|
271
|
+
export interface TrackResult {
|
|
272
|
+
/**
|
|
273
|
+
* How far this signal has actually got. Branch on this, not on `queued`.
|
|
274
|
+
*
|
|
275
|
+
* `"buffered"` — held in THIS process's memory and not yet sent. Lost if
|
|
276
|
+
* the process exits first, so `flush()`/`close()` before it
|
|
277
|
+
* can. No `rawId` exists yet.
|
|
278
|
+
* `"accepted"` — the server has written it to its durable inbox (`202`)
|
|
279
|
+
* and will price it shortly. It can no longer be lost, but
|
|
280
|
+
* it is not billed yet and `usageLog` is null. A rejection
|
|
281
|
+
* discovered during pricing will NOT come back to you here —
|
|
282
|
+
* read it back with `signals.list()`.
|
|
283
|
+
* `"recorded"` — the server priced it inline and `usageLog` is populated.
|
|
284
|
+
* `"dropped"` — the SDK threw it away without sending: the client is
|
|
285
|
+
* closed, or the local queue is full. `onDrop` fired with
|
|
286
|
+
* the reason. This state exists because the alternative was
|
|
287
|
+
* reporting `"buffered"` for a signal that was discarded —
|
|
288
|
+
* a success-shaped answer to a lost billable event.
|
|
289
|
+
*
|
|
290
|
+
* Note what none of these mean: "waited for a price". Even `{ wait: true }`
|
|
291
|
+
* waits only for the server to ACCEPT the signal, never for it to finish
|
|
292
|
+
* pricing — holding a request thread open for a billing round-trip is
|
|
293
|
+
* exactly the cost this design exists to avoid.
|
|
294
|
+
*/
|
|
295
|
+
status: SignalDelivery;
|
|
296
|
+
/**
|
|
297
|
+
* True for both `"buffered"` and `"accepted"` — i.e. "not priced yet".
|
|
298
|
+
*
|
|
299
|
+
* Kept for compatibility, but it collapses two states with opposite failure
|
|
300
|
+
* behaviour (one is lost when the process dies, the other cannot be) into
|
|
301
|
+
* one boolean, which is why {@link TrackResult.status} exists. Prefer that.
|
|
302
|
+
*/
|
|
303
|
+
queued: boolean;
|
|
304
|
+
/** The computed usage log. Non-null ONLY when the server priced the signal
|
|
305
|
+
* inline (`queued: false`); null on a `202` hand-off and absent while the
|
|
306
|
+
* signal sits in the client-side buffer. */
|
|
307
|
+
usageLog?: SerializedUsageLog | null;
|
|
308
|
+
/** The server's durable-queue message id, present only on a `202` hand-off
|
|
309
|
+
* (server-side queueing). Absent for client-side buffering and for inline
|
|
310
|
+
* pricing. */
|
|
311
|
+
messageId?: string;
|
|
312
|
+
/**
|
|
313
|
+
* The signal's durable inbox id, returned by every `/api/v1/signal/*`
|
|
314
|
+
* response — the `202` hand-off AND inline pricing. Present whenever the
|
|
315
|
+
* server has written the request down, which it now does BEFORE validating
|
|
316
|
+
* or pricing it, so a `rawId` means "this signal cannot be lost" even when
|
|
317
|
+
* `usageLog` is null. Quote it when asking why a signal has not been billed;
|
|
318
|
+
* it is the row the dashboard shows as Processing / Pending / Processed.
|
|
319
|
+
*
|
|
320
|
+
* Absent while the signal sits in the client-side buffer (nothing has been
|
|
321
|
+
* sent yet, so the server has no id for it).
|
|
322
|
+
*/
|
|
323
|
+
rawId?: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface UsageList {
|
|
327
|
+
customer: { id: string; name: string; creditAllowance: number };
|
|
328
|
+
totals: {
|
|
329
|
+
count: number;
|
|
330
|
+
cost: number;
|
|
331
|
+
revenue: number;
|
|
332
|
+
profit: number;
|
|
333
|
+
creditsUsed: number;
|
|
334
|
+
};
|
|
335
|
+
logs: SerializedUsageLog[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Customers (mirrors src/lib/customers/api.ts)
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
export interface ApiCustomer {
|
|
343
|
+
id: string;
|
|
344
|
+
slug: string;
|
|
345
|
+
name: string;
|
|
346
|
+
description: string | null;
|
|
347
|
+
email: string | null;
|
|
348
|
+
website: string | null;
|
|
349
|
+
phone: string | null;
|
|
350
|
+
legalName: string | null;
|
|
351
|
+
addressLine1: string | null;
|
|
352
|
+
city: string | null;
|
|
353
|
+
state: string | null;
|
|
354
|
+
country: string | null;
|
|
355
|
+
pincode: string | null;
|
|
356
|
+
taxId: string | null;
|
|
357
|
+
currencyCode: string;
|
|
358
|
+
creditAllowance: number;
|
|
359
|
+
createdAt: string;
|
|
360
|
+
updatedAt: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Writable customer profile fields. `null` clears a column; omit to leave it. */
|
|
364
|
+
export interface CustomerProfileInput {
|
|
365
|
+
name?: string;
|
|
366
|
+
description?: string | null;
|
|
367
|
+
email?: string | null;
|
|
368
|
+
website?: string | null;
|
|
369
|
+
phone?: string | null;
|
|
370
|
+
legalName?: string | null;
|
|
371
|
+
addressLine1?: string | null;
|
|
372
|
+
city?: string | null;
|
|
373
|
+
state?: string | null;
|
|
374
|
+
country?: string | null;
|
|
375
|
+
pincode?: string | null;
|
|
376
|
+
taxId?: string | null;
|
|
377
|
+
/** ISO 4217 (3 letters); must be enabled for the org. */
|
|
378
|
+
currencyCode?: string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** `name` and `email` are required on create; every other field is optional. */
|
|
382
|
+
export type CreateCustomerInput = CustomerProfileInput & {
|
|
383
|
+
name: string;
|
|
384
|
+
email: string;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export interface CustomerList {
|
|
388
|
+
customers: ApiCustomer[];
|
|
389
|
+
nextCursor: string | null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** One credit type's running balance for a customer (an element of
|
|
393
|
+
* `customers.balances().credits`). */
|
|
394
|
+
export interface CreditBalance {
|
|
395
|
+
creditId: string;
|
|
396
|
+
/** The credit's stable agent key — the same key used to record usage and to
|
|
397
|
+
* adjust this balance. */
|
|
398
|
+
agentKey: string;
|
|
399
|
+
creditName: string;
|
|
400
|
+
granted: number;
|
|
401
|
+
used: number;
|
|
402
|
+
remaining: number;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** One outcome's running balance for a customer. Unlike a credit balance it
|
|
406
|
+
* has no `used`, and it carries the outcome's ordered step list. */
|
|
407
|
+
export interface OutcomeBalance {
|
|
408
|
+
outcomeId: string;
|
|
409
|
+
outcomeName: string;
|
|
410
|
+
granted: number;
|
|
411
|
+
remaining: number;
|
|
412
|
+
/** Each step's `agentKey` is what `signals.outcome()` and the outcome adjust
|
|
413
|
+
* API use to address this outcome at runtime. The outcome also has its own
|
|
414
|
+
* separate key ({@link Outcome.agentKey}), but neither of those APIs accepts
|
|
415
|
+
* it — it identifies the outcome in the catalogue, not in a signal. */
|
|
416
|
+
steps: { id: string; name: string; agentKey: string }[];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** One unit's advance allowance for a customer. No `used` field. */
|
|
420
|
+
export interface UnitBalance {
|
|
421
|
+
unitId: string;
|
|
422
|
+
/** The unit's stable agent key — the same key used to record usage and to
|
|
423
|
+
* adjust this balance. */
|
|
424
|
+
agentKey: string;
|
|
425
|
+
unitName: string;
|
|
426
|
+
granted: number;
|
|
427
|
+
remaining: number;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Wallet / credit / outcome / unit balances for a customer's active plan
|
|
431
|
+
* (`customers.balances()`). */
|
|
432
|
+
export interface CustomerBalances {
|
|
433
|
+
customerId: string;
|
|
434
|
+
currency: { code: string; rateUsd: number };
|
|
435
|
+
hasPlan: boolean;
|
|
436
|
+
wallet: { balanceUsd: number } | null;
|
|
437
|
+
credits: CreditBalance[];
|
|
438
|
+
outcomes: OutcomeBalance[];
|
|
439
|
+
units: UnitBalance[];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** The customer's active plan (the `plan` field of `customers.plan()`);
|
|
443
|
+
* `null` when the customer is unsubscribed. */
|
|
444
|
+
export interface CustomerPlanDetails {
|
|
445
|
+
purchaseId: string;
|
|
446
|
+
planId: string | null;
|
|
447
|
+
planName: string;
|
|
448
|
+
planType: PlanType;
|
|
449
|
+
planKind: PlanKind;
|
|
450
|
+
seatScope: "CUSTOMER" | "MEMBER" | null;
|
|
451
|
+
billingCycle: BillingCycle;
|
|
452
|
+
cost: number;
|
|
453
|
+
status: "SCHEDULED" | "ACTIVE";
|
|
454
|
+
billingDate: string;
|
|
455
|
+
spendingLimit: number | null;
|
|
456
|
+
spentThisCycle: number | null;
|
|
457
|
+
meters: { wallet: boolean; credit: boolean; outcome: boolean; unit: boolean };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export interface CustomerPlan {
|
|
461
|
+
customerId: string;
|
|
462
|
+
currency: { code: string; rateUsd: number };
|
|
463
|
+
plan: CustomerPlanDetails | null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
// Portal
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
export interface PortalTokenInput {
|
|
471
|
+
customerId: string;
|
|
472
|
+
ttlSeconds?: number;
|
|
473
|
+
memberEmail?: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export interface PortalToken {
|
|
477
|
+
token: string;
|
|
478
|
+
/** Unix seconds. */
|
|
479
|
+
expiresAt: number;
|
|
480
|
+
/** Seconds until expiry. */
|
|
481
|
+
expiresIn: number;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Plans
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
/** How often a plan bills. `FREE` is a one-time grant; `EVERY_5_MIN` is a test cadence. */
|
|
489
|
+
export type BillingCycle =
|
|
490
|
+
| "MONTHLY"
|
|
491
|
+
| "QUARTERLY"
|
|
492
|
+
| "SEMI_ANNUAL"
|
|
493
|
+
| "YEARLY"
|
|
494
|
+
| "EVERY_5_MIN"
|
|
495
|
+
| "FREE";
|
|
496
|
+
export type PlanType = "WALLET" | "BUNDLE";
|
|
497
|
+
export type PlanKind = "FLAT_PRICE" | "PAY_AS_YOU_GO" | "PAY_PER_SEAT";
|
|
498
|
+
/** A plan component's meter type. */
|
|
499
|
+
/**
|
|
500
|
+
* A plan component's kind.
|
|
501
|
+
*
|
|
502
|
+
* `PRICING_METRIC` is a COMPOSITE component — the server's wire name for it,
|
|
503
|
+
* kept verbatim because that is the string you will actually receive and must
|
|
504
|
+
* send back. It was missing here, which made a `switch` over `PlanComponent.type`
|
|
505
|
+
* look exhaustive to TypeScript while silently skipping every composite.
|
|
506
|
+
*/
|
|
507
|
+
export type ComponentType =
|
|
508
|
+
| "WALLET"
|
|
509
|
+
| "CREDIT"
|
|
510
|
+
| "OUTCOME"
|
|
511
|
+
| "UNIT"
|
|
512
|
+
| "FLAT"
|
|
513
|
+
| "PRICING_METRIC";
|
|
514
|
+
/** ADVANCE bills up-front for the cycle; ARREAR bills for what was consumed. */
|
|
515
|
+
export type BillingMode = "ADVANCE" | "ARREAR";
|
|
516
|
+
|
|
517
|
+
/** Prepaid USD wallet. `amount` is the funded balance (required). Max one
|
|
518
|
+
* wallet component per plan. */
|
|
519
|
+
export interface WalletComponentInput {
|
|
520
|
+
type: "WALLET";
|
|
521
|
+
billingMode: BillingMode;
|
|
522
|
+
amount: number;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** A one-off flat fee — `amount` is required. Max one flat component per plan;
|
|
526
|
+
* not allowed on FREE plans. */
|
|
527
|
+
export interface FlatComponentInput {
|
|
528
|
+
type: "FLAT";
|
|
529
|
+
billingMode: BillingMode;
|
|
530
|
+
amount: number;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** A credit entitlement, referenced by `creditId`. Billed ADVANCE it needs a
|
|
534
|
+
* granted `quantity`; billed ARREAR it meters usage with no upfront grant. */
|
|
535
|
+
export type CreditComponentInput =
|
|
536
|
+
| { type: "CREDIT"; billingMode: "ADVANCE"; creditId: string; quantity: number }
|
|
537
|
+
| { type: "CREDIT"; billingMode: "ARREAR"; creditId: string; quantity?: number | null };
|
|
538
|
+
|
|
539
|
+
/** An outcome entitlement, referenced by `outcomeId`. ADVANCE needs a granted
|
|
540
|
+
* `quantity`; ARREAR meters usage. */
|
|
541
|
+
export type OutcomeComponentInput =
|
|
542
|
+
| { type: "OUTCOME"; billingMode: "ADVANCE"; outcomeId: string; quantity: number }
|
|
543
|
+
| { type: "OUTCOME"; billingMode: "ARREAR"; outcomeId: string; quantity?: number | null };
|
|
544
|
+
|
|
545
|
+
/** A unit entitlement, referenced by `unitId`. ADVANCE needs a granted
|
|
546
|
+
* `quantity`; ARREAR meters usage. */
|
|
547
|
+
export type UnitComponentInput =
|
|
548
|
+
| { type: "UNIT"; billingMode: "ADVANCE"; unitId: string; quantity: number }
|
|
549
|
+
| { type: "UNIT"; billingMode: "ARREAR"; unitId: string; quantity?: number | null };
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* A COMPOSITE sold on the plan, referenced by `pricingMetricId` (the id from
|
|
553
|
+
* `composites.list()`). This is the line that makes a composite bill: the
|
|
554
|
+
* catalogue entry on its own charges nobody, and this component is what turns
|
|
555
|
+
* it into money — `quantity × the composite's price`, frozen into the purchase
|
|
556
|
+
* snapshot at buy time.
|
|
557
|
+
*
|
|
558
|
+
* The two billing modes differ in what `quantity` means:
|
|
559
|
+
* - **ADVANCE** — a PREPAID POOL of `quantity` flat-rate slots, shared
|
|
560
|
+
* across whichever credits / outcomes / units the composite wraps. Each
|
|
561
|
+
* distinct tag value claims one slot on completion; repeat signals under a
|
|
562
|
+
* value reuse the slot they already claimed, at no extra cost. Once every
|
|
563
|
+
* slot is claimed, further occurrences are refused rather than billed.
|
|
564
|
+
* - **ARREAR** — nothing prepaid; completed occurrences are counted on the
|
|
565
|
+
* cycle-end line.
|
|
566
|
+
*
|
|
567
|
+
* `type` is the wire value `"PRICING_METRIC"`, not `"COMPOSITE"`: the product
|
|
568
|
+
* word changed but the stored enum did not, and the API still speaks the old
|
|
569
|
+
* one. That mismatch is the server's, and it is surfaced rather than hidden so
|
|
570
|
+
* a response and a request read the same.
|
|
571
|
+
*/
|
|
572
|
+
export type CompositeComponentInput =
|
|
573
|
+
| {
|
|
574
|
+
type: "PRICING_METRIC";
|
|
575
|
+
billingMode: "ADVANCE";
|
|
576
|
+
pricingMetricId: string;
|
|
577
|
+
quantity: number;
|
|
578
|
+
}
|
|
579
|
+
| {
|
|
580
|
+
type: "PRICING_METRIC";
|
|
581
|
+
billingMode: "ARREAR";
|
|
582
|
+
pricingMetricId: string;
|
|
583
|
+
quantity?: number | null;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* One entitlement line in a plan — a discriminated union on `type` (and, where
|
|
588
|
+
* it matters, `billingMode`) so the per-type fields the server requires are
|
|
589
|
+
* enforced at compile time instead of surfacing as runtime 422s:
|
|
590
|
+
* - CREDIT / OUTCOME / UNIT require their matching id;
|
|
591
|
+
* - PRICING_METRIC (a composite) requires `pricingMetricId`;
|
|
592
|
+
* - CREDIT / OUTCOME / UNIT / PRICING_METRIC require `quantity` when billed
|
|
593
|
+
* ADVANCE;
|
|
594
|
+
* - WALLET / FLAT require `amount`.
|
|
595
|
+
*/
|
|
596
|
+
export type PlanComponentInput =
|
|
597
|
+
| WalletComponentInput
|
|
598
|
+
| FlatComponentInput
|
|
599
|
+
| CreditComponentInput
|
|
600
|
+
| OutcomeComponentInput
|
|
601
|
+
| UnitComponentInput
|
|
602
|
+
| CompositeComponentInput;
|
|
603
|
+
|
|
604
|
+
export interface CreatePlanInput {
|
|
605
|
+
name: string;
|
|
606
|
+
description?: string | null;
|
|
607
|
+
billingCycle: BillingCycle;
|
|
608
|
+
/** Carry an unused balance into the next cycle. Default true. */
|
|
609
|
+
carryForward?: boolean;
|
|
610
|
+
/** ISO 4217 (3 letters). Default "USD". */
|
|
611
|
+
currencyCode?: string;
|
|
612
|
+
isActive?: boolean;
|
|
613
|
+
/** At least one component. FREE plans must be ADVANCE-only with no FLAT component. */
|
|
614
|
+
components: PlanComponentInput[];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** A full-rewrite update carries the complete plan (same shape as create). */
|
|
618
|
+
export type UpdatePlanInput = CreatePlanInput;
|
|
619
|
+
|
|
620
|
+
export interface PlanComponent {
|
|
621
|
+
id: string;
|
|
622
|
+
type: ComponentType;
|
|
623
|
+
billingMode: BillingMode;
|
|
624
|
+
creditId: string | null;
|
|
625
|
+
outcomeId: string | null;
|
|
626
|
+
unitId: string | null;
|
|
627
|
+
/** The composite this component sells, on a `PRICING_METRIC` component.
|
|
628
|
+
* Without it a plan read back could not be written back: the write shape
|
|
629
|
+
* ({@link CompositeComponentInput}) requires this id, so a read-modify-write
|
|
630
|
+
* silently dropped every composite component. */
|
|
631
|
+
pricingMetricId: string | null;
|
|
632
|
+
targetName: string | null;
|
|
633
|
+
quantity: number | null;
|
|
634
|
+
amount: number | null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export interface Plan {
|
|
638
|
+
id: string;
|
|
639
|
+
name: string;
|
|
640
|
+
description: string | null;
|
|
641
|
+
type: PlanType;
|
|
642
|
+
kind: PlanKind;
|
|
643
|
+
billingCycle: BillingCycle;
|
|
644
|
+
cost: number;
|
|
645
|
+
/** Signed adjustment applied to the plan's headline price (default 0). */
|
|
646
|
+
priceAdjustment: number;
|
|
647
|
+
/** When true, ARREAR consumption is funded from the customer's wallet as it
|
|
648
|
+
* happens instead of being invoiced at cycle end. All-or-nothing per plan. */
|
|
649
|
+
walletFundedArrear: boolean;
|
|
650
|
+
currencyCode: string;
|
|
651
|
+
isActive: boolean;
|
|
652
|
+
components: PlanComponent[];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** The richer plan returned by `plans.get()` (adds aggregate stats). */
|
|
656
|
+
export interface PlanDetail extends Plan {
|
|
657
|
+
createdAt: string;
|
|
658
|
+
createdByName: string | null;
|
|
659
|
+
customerCount: number;
|
|
660
|
+
invoiceCount: number;
|
|
661
|
+
openInvoiceCount: number;
|
|
662
|
+
stats: { totalInvoiced: number; totalCollected: number; openBalance: number };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
// Purchases (subscriptions)
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
|
|
669
|
+
export type PurchaseStatus = "SCHEDULED" | "ACTIVE" | "CANCELLED";
|
|
670
|
+
|
|
671
|
+
export interface CreatePurchaseInput {
|
|
672
|
+
customerId: string;
|
|
673
|
+
planId: string;
|
|
674
|
+
/** `YYYY-MM-DD`. Defaults to today; must be a valid non-past date. */
|
|
675
|
+
billingDate?: string;
|
|
676
|
+
notes?: string;
|
|
677
|
+
autoPayment?: boolean;
|
|
678
|
+
/** Auto-void the purchase if unpaid after N minutes. */
|
|
679
|
+
voidAfterMinutes?: number;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export interface Purchase {
|
|
683
|
+
id: string;
|
|
684
|
+
purchaseNumber: string;
|
|
685
|
+
customerId: string;
|
|
686
|
+
customerName: string;
|
|
687
|
+
customerLogoUrl: string | null;
|
|
688
|
+
planId: string | null;
|
|
689
|
+
planName: string;
|
|
690
|
+
planType: PlanType;
|
|
691
|
+
hasWallet: boolean;
|
|
692
|
+
hasCredit: boolean;
|
|
693
|
+
hasOutcome: boolean;
|
|
694
|
+
hasUnit: boolean;
|
|
695
|
+
/** True when the purchased plan sells at least one composite. */
|
|
696
|
+
hasComposite: boolean;
|
|
697
|
+
billingCycle: BillingCycle;
|
|
698
|
+
cost: number;
|
|
699
|
+
currencyCode: string;
|
|
700
|
+
billingDate: string;
|
|
701
|
+
nextBillingDate: string;
|
|
702
|
+
status: PurchaseStatus;
|
|
703
|
+
voidAfterMinutes: number;
|
|
704
|
+
autoPayment: boolean;
|
|
705
|
+
autoPaymentPauseReason: string | null;
|
|
706
|
+
notes: string | null;
|
|
707
|
+
createdAt: string;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** The purchase returned by `purchases.get()` — a different, detail-oriented shape. */
|
|
711
|
+
export interface PurchaseDetail {
|
|
712
|
+
id: string;
|
|
713
|
+
purchaseNumber: string;
|
|
714
|
+
status: PurchaseStatus;
|
|
715
|
+
cost: number;
|
|
716
|
+
billingCycle: BillingCycle;
|
|
717
|
+
planName: string;
|
|
718
|
+
billingDate: string;
|
|
719
|
+
nextBillingDate: string;
|
|
720
|
+
createdAt: string;
|
|
721
|
+
createdByName: string;
|
|
722
|
+
customer: { id: string; name: string; logoUrl: string | null };
|
|
723
|
+
voidAfterMinutes: number;
|
|
724
|
+
notes: string | null;
|
|
725
|
+
autoPayment: boolean;
|
|
726
|
+
autoPaymentPauseReason: string | null;
|
|
727
|
+
autoPaymentPausedAt: string | null;
|
|
728
|
+
plan: { id: string } | null;
|
|
729
|
+
stats: { invoiceCount: number; paidCount: number };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
// Invoices & Payments
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
export type InvoiceStatus = "OPEN" | "PAID" | "VOID";
|
|
737
|
+
|
|
738
|
+
/** Trimmed row from `invoices.list()`. */
|
|
739
|
+
export interface InvoiceListRow {
|
|
740
|
+
id: string;
|
|
741
|
+
invoiceNumber: string;
|
|
742
|
+
purchaseNumber: string | null;
|
|
743
|
+
purchaseId: string | null;
|
|
744
|
+
customerId: string;
|
|
745
|
+
customerName: string;
|
|
746
|
+
planName: string;
|
|
747
|
+
billingCycle: BillingCycle;
|
|
748
|
+
cycleIndex: number;
|
|
749
|
+
amount: number;
|
|
750
|
+
currencyCode: string;
|
|
751
|
+
status: InvoiceStatus;
|
|
752
|
+
issuedAt: string;
|
|
753
|
+
paidAt: string | null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** A billing party (your org, or the customer) on an invoice. */
|
|
757
|
+
export interface InvoiceParty {
|
|
758
|
+
name: string;
|
|
759
|
+
logoUrl: string | null;
|
|
760
|
+
legalName: string | null;
|
|
761
|
+
email: string | null;
|
|
762
|
+
phone: string | null;
|
|
763
|
+
website: string | null;
|
|
764
|
+
taxId: string | null;
|
|
765
|
+
addressLine1: string | null;
|
|
766
|
+
city: string | null;
|
|
767
|
+
state: string | null;
|
|
768
|
+
country: string | null;
|
|
769
|
+
pincode: string | null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface InvoiceLineItem {
|
|
773
|
+
kind: "credit" | "outcome";
|
|
774
|
+
label: string;
|
|
775
|
+
quantity: number;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** The plan snapshot embedded in a full invoice. */
|
|
779
|
+
export interface InvoicePlanSnapshot {
|
|
780
|
+
name: string;
|
|
781
|
+
type: PlanType;
|
|
782
|
+
hasWallet: boolean;
|
|
783
|
+
hasCredit: boolean;
|
|
784
|
+
hasOutcome: boolean;
|
|
785
|
+
hasUnit: boolean;
|
|
786
|
+
kind: PlanKind;
|
|
787
|
+
billingCycle: BillingCycle;
|
|
788
|
+
cost: number;
|
|
789
|
+
currencyCode: string;
|
|
790
|
+
spendingLimit: number | null;
|
|
791
|
+
seatScope: "CUSTOMER" | "MEMBER" | null;
|
|
792
|
+
carryForward: boolean;
|
|
793
|
+
billingDate: string;
|
|
794
|
+
status: "SCHEDULED" | "ACTIVE";
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** The full invoice from `invoices.get()` (also the `payment` in `payments.get()`). */
|
|
798
|
+
export interface Invoice {
|
|
799
|
+
id: string;
|
|
800
|
+
invoiceNumber: string;
|
|
801
|
+
paymentNumber: string | null;
|
|
802
|
+
purchaseNumber: string | null;
|
|
803
|
+
customerId: string;
|
|
804
|
+
customerName: string;
|
|
805
|
+
customerSlug: string;
|
|
806
|
+
customerParty: InvoiceParty;
|
|
807
|
+
orgParty: InvoiceParty;
|
|
808
|
+
purchaseId: string | null;
|
|
809
|
+
stream: BillingMode;
|
|
810
|
+
planId: string | null;
|
|
811
|
+
planName: string;
|
|
812
|
+
planType: PlanType;
|
|
813
|
+
plan: InvoicePlanSnapshot | null;
|
|
814
|
+
billingCycle: BillingCycle;
|
|
815
|
+
cycleIndex: number;
|
|
816
|
+
lineItems: InvoiceLineItem[];
|
|
817
|
+
hasWallet: boolean;
|
|
818
|
+
hasCredit: boolean;
|
|
819
|
+
hasOutcome: boolean;
|
|
820
|
+
hasUnit: boolean;
|
|
821
|
+
amount: number;
|
|
822
|
+
currencyCode: string;
|
|
823
|
+
currencyRateUsd: number;
|
|
824
|
+
status: InvoiceStatus;
|
|
825
|
+
issuedAt: string;
|
|
826
|
+
paidAt: string | null;
|
|
827
|
+
voidedAt: string | null;
|
|
828
|
+
/** One of the server `PaymentMode` values (e.g. "CASH", "STRIPE", …). */
|
|
829
|
+
paymentMode: string | null;
|
|
830
|
+
paymentReference: string | null;
|
|
831
|
+
stripePaymentIntentId: string | null;
|
|
832
|
+
autoPaid: boolean;
|
|
833
|
+
notes: string | null;
|
|
834
|
+
createdAt: string;
|
|
835
|
+
createdByName: string;
|
|
836
|
+
paidByName: string | null;
|
|
837
|
+
voidedByName: string | null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** A short `{ id, name }` plan reference alongside a full invoice/payment. */
|
|
841
|
+
export interface PlanRef {
|
|
842
|
+
id: string;
|
|
843
|
+
name: string;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export interface InvoiceDetail {
|
|
847
|
+
invoice: Invoice;
|
|
848
|
+
plan: PlanRef | null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** A hosted pay link for an OPEN invoice. */
|
|
852
|
+
export interface PayLink {
|
|
853
|
+
url: string;
|
|
854
|
+
invoiceId: string;
|
|
855
|
+
invoiceNumber: string;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Trimmed row from `payments.list()` (a payment is a PAID invoice). */
|
|
859
|
+
export interface PaymentRow {
|
|
860
|
+
id: string;
|
|
861
|
+
paymentNumber: string;
|
|
862
|
+
invoiceNumber: string;
|
|
863
|
+
purchaseNumber: string | null;
|
|
864
|
+
customerId: string;
|
|
865
|
+
customerName: string;
|
|
866
|
+
planName: string;
|
|
867
|
+
amount: number;
|
|
868
|
+
currencyCode: string;
|
|
869
|
+
paymentMode: string | null;
|
|
870
|
+
paymentReference: string | null;
|
|
871
|
+
paidAt: string;
|
|
872
|
+
paidByName: string | null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export interface PaymentDetail {
|
|
876
|
+
payment: Invoice;
|
|
877
|
+
plan: PlanRef | null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ---------------------------------------------------------------------------
|
|
881
|
+
// Customer members
|
|
882
|
+
// ---------------------------------------------------------------------------
|
|
883
|
+
|
|
884
|
+
/** A customer member as returned by `listMembers` (`GET .../members`). */
|
|
885
|
+
export interface Member {
|
|
886
|
+
id: string;
|
|
887
|
+
name: string;
|
|
888
|
+
email: string | null;
|
|
889
|
+
/** True once the member has linked a login. */
|
|
890
|
+
isLinked: boolean;
|
|
891
|
+
joinedAt: string;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** A customer member as returned by `addMember` / `updateMember`. */
|
|
895
|
+
export interface MemberProfile {
|
|
896
|
+
id: string;
|
|
897
|
+
name: string;
|
|
898
|
+
email: string | null;
|
|
899
|
+
joinedAt: string;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** The member removed by `removeMember` (`DELETE .../members/:memberId`). */
|
|
903
|
+
export interface RemovedMember {
|
|
904
|
+
name: string;
|
|
905
|
+
email: string | null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export interface AddMemberInput {
|
|
909
|
+
name: string;
|
|
910
|
+
/** Must be unique (case-insensitively) among the customer's active members. */
|
|
911
|
+
email?: string;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export interface UpdateMemberInput {
|
|
915
|
+
name: string;
|
|
916
|
+
/** Must be unique (case-insensitively) among the customer's active members. */
|
|
917
|
+
email?: string;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ---------------------------------------------------------------------------
|
|
921
|
+
// Wallet & balance adjustments (customer-scoped)
|
|
922
|
+
// ---------------------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
export type WalletTransactionType = "CREDIT" | "DEBIT";
|
|
925
|
+
|
|
926
|
+
/** What produced a wallet entry. `null` on rows written before the column
|
|
927
|
+
* existed. */
|
|
928
|
+
export type WalletTransactionSource =
|
|
929
|
+
| "TOPUP"
|
|
930
|
+
| "USAGE"
|
|
931
|
+
| "SETTLE"
|
|
932
|
+
| "ADJUSTMENT";
|
|
933
|
+
|
|
934
|
+
/** The entitlement kinds a wallet-funded metering debit can name. */
|
|
935
|
+
export type WalletEntitlementKind = "CREDIT" | "OUTCOME" | "UNIT";
|
|
936
|
+
|
|
937
|
+
export interface WalletTransaction {
|
|
938
|
+
id: string;
|
|
939
|
+
type: WalletTransactionType;
|
|
940
|
+
/** What produced the entry. Null on rows older than the column. */
|
|
941
|
+
source: WalletTransactionSource | null;
|
|
942
|
+
amount: number;
|
|
943
|
+
amountUsd: number;
|
|
944
|
+
currencyCode: string;
|
|
945
|
+
currencyRateUsd: number;
|
|
946
|
+
notes: string | null;
|
|
947
|
+
batchId: string | null;
|
|
948
|
+
createdAt: string;
|
|
949
|
+
createdByName: string | null;
|
|
950
|
+
/** Set only on a wallet-funded metering debit: which kind of entitlement was
|
|
951
|
+
* consumed. Null on top-ups and manual adjustments. */
|
|
952
|
+
entitlementType: WalletEntitlementKind | null;
|
|
953
|
+
/** Id of the credit / outcome / unit consumed. Null unless
|
|
954
|
+
* `entitlementType` is set. */
|
|
955
|
+
targetId: string | null;
|
|
956
|
+
/** Its name at the time of the debit. */
|
|
957
|
+
targetName: string | null;
|
|
958
|
+
/** The signal this debit paid for (credit / outcome). In wallet-funded mode
|
|
959
|
+
* no invoice exists, so this is the only trail from a charge to its cause. */
|
|
960
|
+
usageLogId: string | null;
|
|
961
|
+
/** The unit event this debit paid for. */
|
|
962
|
+
unitUsageLogId: string | null;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export interface WalletList {
|
|
966
|
+
balanceUsd: number;
|
|
967
|
+
transactions: WalletTransaction[];
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export interface WalletEntryInput {
|
|
971
|
+
/** DEBIT spends, CREDIT tops up. */
|
|
972
|
+
type: WalletTransactionType;
|
|
973
|
+
/** Always positive; the sign comes from `type`. */
|
|
974
|
+
amount: number;
|
|
975
|
+
notes?: string;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** Grant (positive) or claw back (negative) a credit balance. `delta` must be a
|
|
979
|
+
* non-zero whole number. The credit is addressed by its stable `agentKey`. */
|
|
980
|
+
export interface CreditAdjustInput {
|
|
981
|
+
/** The credit's stable agent key (the same key used to record usage). */
|
|
982
|
+
agentKey: string;
|
|
983
|
+
delta: number;
|
|
984
|
+
notes?: string;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Grant/claw an outcome balance. `delta` is a non-zero whole number; floored at
|
|
988
|
+
* 0. The outcome is addressed here by a STEP's `agentKey`, which resolves to its
|
|
989
|
+
* parent outcome — NOT by the outcome's own `agentKey` ({@link Outcome.agentKey}),
|
|
990
|
+
* which this endpoint does not accept. */
|
|
991
|
+
export interface OutcomeAdjustInput {
|
|
992
|
+
/** An outcome STEP's stable agent key — identifies the parent outcome. Passing
|
|
993
|
+
* the outcome's own `agentKey` here fails with `422`. */
|
|
994
|
+
agentKey: string;
|
|
995
|
+
delta: number;
|
|
996
|
+
notes?: string;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** Grant/claw a unit balance. `delta` is non-zero (fractional allowed); floored
|
|
1000
|
+
* at 0. The unit is addressed by its stable `agentKey`. */
|
|
1001
|
+
export interface UnitAdjustInput {
|
|
1002
|
+
/** The unit's stable agent key (the same key used to record usage). */
|
|
1003
|
+
agentKey: string;
|
|
1004
|
+
delta: number;
|
|
1005
|
+
notes?: string;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ---------------------------------------------------------------------------
|
|
1009
|
+
// Entitlement catalogs: Credits, Outcomes, Units
|
|
1010
|
+
// ---------------------------------------------------------------------------
|
|
1011
|
+
|
|
1012
|
+
/** A per-model token bundle attached to a credit or outcome step. */
|
|
1013
|
+
export interface ModelBundleEntry {
|
|
1014
|
+
orgModelId: string;
|
|
1015
|
+
/** Display label; the server defaults it from the model when omitted. */
|
|
1016
|
+
modelName?: string;
|
|
1017
|
+
tokens: number;
|
|
1018
|
+
input: number;
|
|
1019
|
+
output: number;
|
|
1020
|
+
cache: number;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** One point on a cost-over-time analytics series. */
|
|
1024
|
+
export interface CostPoint {
|
|
1025
|
+
/** UTC day, `YYYY-MM-DD`. */
|
|
1026
|
+
dateIso: string;
|
|
1027
|
+
/** Short display label, e.g. "Jun 24". */
|
|
1028
|
+
label: string;
|
|
1029
|
+
/** Average cost that day, or `null` on days with no usage. */
|
|
1030
|
+
cost: number | null;
|
|
1031
|
+
reqs: number;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** Summary stats across a cost-over-time series. */
|
|
1035
|
+
export interface CostStats {
|
|
1036
|
+
activeDays: number;
|
|
1037
|
+
avgCost: number | null;
|
|
1038
|
+
peakCost: number | null;
|
|
1039
|
+
troughCost: number | null;
|
|
1040
|
+
deviationPct: number | null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** Cost-over-time analytics for a credit (`credits.get().analytics`). */
|
|
1044
|
+
export interface CreditAnalytics {
|
|
1045
|
+
basePrice: number;
|
|
1046
|
+
pricePerCredit: number;
|
|
1047
|
+
marginPercent: number;
|
|
1048
|
+
points: CostPoint[];
|
|
1049
|
+
stats: CostStats;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** Cost-over-time analytics for an outcome (`outcomes.get().analytics`).
|
|
1053
|
+
* Each point adds `completions` (workflows finished that day). */
|
|
1054
|
+
export interface OutcomeAnalytics {
|
|
1055
|
+
basePrice: number;
|
|
1056
|
+
pricePerOutcome: number;
|
|
1057
|
+
marginPercent: number;
|
|
1058
|
+
points: (CostPoint & { completions: number })[];
|
|
1059
|
+
stats: CostStats;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export interface Credit {
|
|
1063
|
+
id: string;
|
|
1064
|
+
name: string;
|
|
1065
|
+
agentKey: string;
|
|
1066
|
+
description: string | null;
|
|
1067
|
+
tokensPerCredit: number;
|
|
1068
|
+
basePrice: number;
|
|
1069
|
+
marginPercent: number;
|
|
1070
|
+
pricePerCredit: number;
|
|
1071
|
+
isActive: boolean;
|
|
1072
|
+
createdAt: string;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** `credits.get()` — adds the model bundle plus usage stats/analytics. */
|
|
1076
|
+
export interface CreditDetail extends Credit {
|
|
1077
|
+
modelBundle: ModelBundleEntry[];
|
|
1078
|
+
createdByName: string;
|
|
1079
|
+
stats: {
|
|
1080
|
+
customerCount: number;
|
|
1081
|
+
planCount: number;
|
|
1082
|
+
totalGranted: number;
|
|
1083
|
+
totalUsed: number;
|
|
1084
|
+
revenue: number;
|
|
1085
|
+
usageLogCount: number;
|
|
1086
|
+
};
|
|
1087
|
+
analytics: CreditAnalytics;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export interface CreateCreditInput {
|
|
1091
|
+
name: string;
|
|
1092
|
+
/** Lowercased stable key used by `signals.credit({ agentKey })`. */
|
|
1093
|
+
agentKey: string;
|
|
1094
|
+
basePrice: number;
|
|
1095
|
+
marginPercent: number;
|
|
1096
|
+
pricePerCredit: number;
|
|
1097
|
+
description?: string;
|
|
1098
|
+
tokensPerCredit?: number;
|
|
1099
|
+
modelBundle?: ModelBundleEntry[];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export type UpdateCreditInput = CreateCreditInput;
|
|
1103
|
+
|
|
1104
|
+
export interface OutcomeStepInput {
|
|
1105
|
+
id?: string;
|
|
1106
|
+
name: string;
|
|
1107
|
+
/** Lowercased stable key used by `signals.outcome({ agentKey })`. */
|
|
1108
|
+
agentKey: string;
|
|
1109
|
+
basePrice: number;
|
|
1110
|
+
modelBundle?: ModelBundleEntry[];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export interface Outcome {
|
|
1114
|
+
id: string;
|
|
1115
|
+
name: string;
|
|
1116
|
+
/**
|
|
1117
|
+
* The OUTCOME's own stable agent key — its durable identity, mirroring
|
|
1118
|
+
* {@link Credit.agentKey}. Lowercased `[a-z0-9._-]`, max 80 chars, unique per
|
|
1119
|
+
* organisation. A rename never changes it.
|
|
1120
|
+
*
|
|
1121
|
+
* Distinct from each step's `agentKey` in `steps`, and in a SEPARATE
|
|
1122
|
+
* namespace: usage is always reported against a STEP's key
|
|
1123
|
+
* (`signals.outcome({ agentKey })`), never this one. This key identifies the
|
|
1124
|
+
* outcome itself when creating or updating it.
|
|
1125
|
+
*/
|
|
1126
|
+
agentKey: string;
|
|
1127
|
+
description: string | null;
|
|
1128
|
+
isActive: boolean;
|
|
1129
|
+
marginPercent: number;
|
|
1130
|
+
pricePerOutcome: number;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** `outcomes.get()` — adds steps plus in-flight/completed stats. */
|
|
1134
|
+
export interface OutcomeDetail extends Outcome {
|
|
1135
|
+
/** Only the DETAIL row carries these — `outcomes.list()` returns neither.
|
|
1136
|
+
* They were previously declared on {@link Outcome} itself, where
|
|
1137
|
+
* `createdAt` was typed as a required `string` that is always `undefined`
|
|
1138
|
+
* in practice. */
|
|
1139
|
+
createdAt: string;
|
|
1140
|
+
createdByName: string | null;
|
|
1141
|
+
steps: { name: string; agentKey: string }[];
|
|
1142
|
+
stats: {
|
|
1143
|
+
stepCount: number;
|
|
1144
|
+
customerCount: number;
|
|
1145
|
+
inFlight: number;
|
|
1146
|
+
completed: number;
|
|
1147
|
+
totalRemaining: number;
|
|
1148
|
+
};
|
|
1149
|
+
analytics: OutcomeAnalytics;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
export interface CreateOutcomeInput {
|
|
1153
|
+
name: string;
|
|
1154
|
+
/**
|
|
1155
|
+
* The OUTCOME's own stable agent key — REQUIRED. Lowercased `[a-z0-9._-]`,
|
|
1156
|
+
* max 80 chars, unique per organisation (the server lowercases and trims it).
|
|
1157
|
+
*
|
|
1158
|
+
* Separate namespace from `steps[].agentKey`: this names the outcome, a step
|
|
1159
|
+
* key names a step and is what `signals.outcome()` reports against. Omitting
|
|
1160
|
+
* it is rejected with a `ValidationError` (400).
|
|
1161
|
+
*/
|
|
1162
|
+
agentKey: string;
|
|
1163
|
+
basePrice: number;
|
|
1164
|
+
marginPercent: number;
|
|
1165
|
+
pricePerOutcome: number;
|
|
1166
|
+
description?: string | null;
|
|
1167
|
+
isActive?: boolean;
|
|
1168
|
+
/** 1–50 steps; step names and agent keys must be unique. */
|
|
1169
|
+
steps: OutcomeStepInput[];
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
export type UpdateOutcomeInput = CreateOutcomeInput;
|
|
1173
|
+
|
|
1174
|
+
export type UnitPricingType = "FLAT" | "SLAB" | "VOLUME";
|
|
1175
|
+
|
|
1176
|
+
/** A tier for SLAB/VOLUME pricing; only the last tier may have `upTo: null`. */
|
|
1177
|
+
export interface UnitTierInput {
|
|
1178
|
+
id?: string;
|
|
1179
|
+
upTo: number | null;
|
|
1180
|
+
price: number;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export interface UnitTier {
|
|
1184
|
+
id: string;
|
|
1185
|
+
upTo: number | null;
|
|
1186
|
+
price: number;
|
|
1187
|
+
order: number;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export interface Unit {
|
|
1191
|
+
id: string;
|
|
1192
|
+
/** Stable per-org report key — what `signals.unit({ agentKey })` sends.
|
|
1193
|
+
* The ONLY identifier accepted when reporting unit consumption. */
|
|
1194
|
+
agentKey: string;
|
|
1195
|
+
name: string;
|
|
1196
|
+
description: string | null;
|
|
1197
|
+
pricingType: UnitPricingType;
|
|
1198
|
+
flatPrice: number;
|
|
1199
|
+
tiers: UnitTier[];
|
|
1200
|
+
isActive: boolean;
|
|
1201
|
+
createdAt: string;
|
|
1202
|
+
createdByName: string | null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** One row in a unit's linked-customer sample (`units.get().customers`). */
|
|
1206
|
+
export interface UnitCustomerRow {
|
|
1207
|
+
customerId: string;
|
|
1208
|
+
name: string;
|
|
1209
|
+
legalName: string | null;
|
|
1210
|
+
slug: string;
|
|
1211
|
+
logoUrl: string | null;
|
|
1212
|
+
granted: number;
|
|
1213
|
+
used: number;
|
|
1214
|
+
remaining: number;
|
|
1215
|
+
currentlyMetered: boolean;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/** `units.get()` — adds linked-customer sample plus stats.
|
|
1219
|
+
* `createdByName` is inherited from `Unit`. */
|
|
1220
|
+
export interface UnitDetail extends Unit {
|
|
1221
|
+
stats: {
|
|
1222
|
+
planCount: number;
|
|
1223
|
+
customerCount: number;
|
|
1224
|
+
totalRemaining: number;
|
|
1225
|
+
totalUsed: number;
|
|
1226
|
+
};
|
|
1227
|
+
/** First page (≤10) of customers currently metered on this unit. */
|
|
1228
|
+
customers: UnitCustomerRow[];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/** Flat per-event pricing — a single `flatPrice` (defaults to 0), no tiers. */
|
|
1232
|
+
export interface FlatUnitInput {
|
|
1233
|
+
name: string;
|
|
1234
|
+
/** Stable per-org report key (`signals.unit({ agentKey })`); `[a-z0-9._-]`. */
|
|
1235
|
+
agentKey: string;
|
|
1236
|
+
pricingType: "FLAT";
|
|
1237
|
+
/** Price per event; defaults to 0. */
|
|
1238
|
+
flatPrice?: number;
|
|
1239
|
+
description?: string | null;
|
|
1240
|
+
isActive?: boolean;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Tiered pricing (SLAB or VOLUME) — at least one tier is required. */
|
|
1244
|
+
export interface TieredUnitInput {
|
|
1245
|
+
name: string;
|
|
1246
|
+
/** Stable per-org report key (`signals.unit({ agentKey })`); `[a-z0-9._-]`. */
|
|
1247
|
+
agentKey: string;
|
|
1248
|
+
pricingType: "SLAB" | "VOLUME";
|
|
1249
|
+
/** At least one tier (≤50). Only the last tier may have `upTo: null`. */
|
|
1250
|
+
tiers: UnitTierInput[];
|
|
1251
|
+
description?: string | null;
|
|
1252
|
+
isActive?: boolean;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Create a unit type — a discriminated union on `pricingType`: `FLAT` takes an
|
|
1257
|
+
* optional `flatPrice` (defaults to 0); `SLAB`/`VOLUME` require `tiers`, so the
|
|
1258
|
+
* server's "add at least one tier" rule is enforced at compile time.
|
|
1259
|
+
*/
|
|
1260
|
+
export type CreateUnitInput = FlatUnitInput | TieredUnitInput;
|
|
1261
|
+
|
|
1262
|
+
export type UpdateUnitInput = CreateUnitInput;
|
|
1263
|
+
|
|
1264
|
+
// ---------------------------------------------------------------------------
|
|
1265
|
+
// Unit consumption (recorded via signals; distinct from catalog `Unit`)
|
|
1266
|
+
// ---------------------------------------------------------------------------
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Record one unit-metered event. The unit is addressed by its `agentKey` (the
|
|
1270
|
+
* stable key set when the unit was created) — the ONLY accepted identifier;
|
|
1271
|
+
* the display name is deliberately not accepted, so a rename can never redirect
|
|
1272
|
+
* a live integration. Keys are matched exactly (they are stored lowercased).
|
|
1273
|
+
*/
|
|
1274
|
+
export interface UnitInput {
|
|
1275
|
+
customerId: string;
|
|
1276
|
+
agentKey: string;
|
|
1277
|
+
member?: string;
|
|
1278
|
+
/**
|
|
1279
|
+
* How many units this one report covers. Defaults to 1 server-side, so a
|
|
1280
|
+
* caller reporting one unit per call simply omits it. Whole numbers, at most
|
|
1281
|
+
* 1,000,000 per call.
|
|
1282
|
+
*
|
|
1283
|
+
* On an ADVANCE (prepaid) plan the draw is all-or-nothing: asking for more
|
|
1284
|
+
* than remains is refused IN FULL rather than partially filled.
|
|
1285
|
+
*/
|
|
1286
|
+
quantity?: number;
|
|
1287
|
+
/** Optional composite this unit report belongs to. See {@link CompositeTag}. */
|
|
1288
|
+
composite?: CompositeTag;
|
|
1289
|
+
/**
|
|
1290
|
+
* Idempotency key for safe retries — a repeat carrying the same key returns
|
|
1291
|
+
* the original result instead of drawing the balance down twice.
|
|
1292
|
+
*
|
|
1293
|
+
* New in the `/api/v1/signal/unit` route: the legacy `/api/v1/units` had no
|
|
1294
|
+
* idempotency at all, so a retried unit report double-charged.
|
|
1295
|
+
*
|
|
1296
|
+
* **Auto-generated (UUID v4) if omitted** and reused across every retry of
|
|
1297
|
+
* this call, exactly like a signal's. That matters more here than it looks:
|
|
1298
|
+
* unit reports opt into transport retries, so without a key one 500-then-200
|
|
1299
|
+
* is two distinct billable reports.
|
|
1300
|
+
*
|
|
1301
|
+
* Supply your own — a stable id for this logical event — to also dedup
|
|
1302
|
+
* across process restarts or at-least-once redelivery, which an in-memory
|
|
1303
|
+
* auto key cannot survive.
|
|
1304
|
+
*/
|
|
1305
|
+
idempotencyKey?: string;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* The result of `signals.unit()`.
|
|
1310
|
+
*
|
|
1311
|
+
* Shaped like {@link TrackResult} and for the same reason: `/api/v1/signal/unit`
|
|
1312
|
+
* is durable, so it answers `202` with no priced event when the server hands
|
|
1313
|
+
* the report to its queue, and `200` with `unitUsage` when it prices inline.
|
|
1314
|
+
* The legacy `/api/v1/units` always priced inline, which is why this used to be
|
|
1315
|
+
* a bare `{ unitUsage }`. Branch on `queued`; never assume `unitUsage`.
|
|
1316
|
+
*/
|
|
1317
|
+
export interface UnitTrackResult {
|
|
1318
|
+
/** How far the report got. Never `"buffered"` — unit reports are always sent
|
|
1319
|
+
* immediately, not queued client-side. See {@link TrackResult.status}. */
|
|
1320
|
+
status: Extract<SignalDelivery, "accepted" | "recorded">;
|
|
1321
|
+
/** True when the report was accepted but not yet priced (`202`). Prefer
|
|
1322
|
+
* {@link UnitTrackResult.status}. */
|
|
1323
|
+
queued: boolean;
|
|
1324
|
+
/** The priced unit event. Non-null ONLY when the server priced inline. */
|
|
1325
|
+
unitUsage?: SerializedUnitUsage | null;
|
|
1326
|
+
/** The durable-queue message id, present only on a `202`. */
|
|
1327
|
+
messageId?: string;
|
|
1328
|
+
/** The durable inbox id — see {@link TrackResult.rawId}. */
|
|
1329
|
+
rawId?: string;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Input for `signals.completeComposite()` — closes ONE occurrence of a
|
|
1334
|
+
* composite, which is the moment it is billed.
|
|
1335
|
+
*/
|
|
1336
|
+
export interface CompositeCompletionInput {
|
|
1337
|
+
customerId: string;
|
|
1338
|
+
/** Which occurrence to close: the composite's `refId` and the correlation
|
|
1339
|
+
* value the signals were tagged with. */
|
|
1340
|
+
composite: CompositeTag;
|
|
1341
|
+
/** Idempotency key, auto-generated if omitted. Closing twice is already a
|
|
1342
|
+
* safe no-op, so this only collapses retries into one recorded attempt
|
|
1343
|
+
* rather than protecting against a double charge. */
|
|
1344
|
+
idempotencyKey?: string;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ---------------------------------------------------------------------------
|
|
1348
|
+
// Composites catalog (mirrors /api/v1/composites)
|
|
1349
|
+
// ---------------------------------------------------------------------------
|
|
1350
|
+
|
|
1351
|
+
/** One catalogue item a composite is restricted to. `agentKey` is what a
|
|
1352
|
+
* signal names, so it is the field you actually integrate against. */
|
|
1353
|
+
export interface CompositeCatalogueRef {
|
|
1354
|
+
id: string;
|
|
1355
|
+
name: string;
|
|
1356
|
+
agentKey: string;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/** One composite in the org's catalogue — `cnk.composites.list()`. */
|
|
1360
|
+
export interface Composite {
|
|
1361
|
+
id: string;
|
|
1362
|
+
name: string;
|
|
1363
|
+
/** The stable identifier you put in a signal's `composite` block as `ref`. */
|
|
1364
|
+
refId: string;
|
|
1365
|
+
description: string | null;
|
|
1366
|
+
/** USD charged per occurrence WHEN A PLAN SELLS THIS as a composite
|
|
1367
|
+
* component (`quantity × price`, frozen into the purchase at buy time). The
|
|
1368
|
+
* catalogue row alone bills nothing. */
|
|
1369
|
+
price: number;
|
|
1370
|
+
/** False once archived. An archived composite stops NEW signals resolving to
|
|
1371
|
+
* it but keeps every row it already owns. */
|
|
1372
|
+
isActive: boolean;
|
|
1373
|
+
createdAt: string;
|
|
1374
|
+
/**
|
|
1375
|
+
* The credits / outcomes / units this composite is restricted to — only
|
|
1376
|
+
* signals naming one of these may carry its tag.
|
|
1377
|
+
*
|
|
1378
|
+
* All three lists empty means UNRESTRICTED (it accepts any of the org's
|
|
1379
|
+
* traffic under this tag). Nothing created through the API can be
|
|
1380
|
+
* unrestricted, but rows predating that rule exist — so don't assume
|
|
1381
|
+
* non-empty.
|
|
1382
|
+
*/
|
|
1383
|
+
entitlements: {
|
|
1384
|
+
credits: CompositeCatalogueRef[];
|
|
1385
|
+
outcomes: CompositeCatalogueRef[];
|
|
1386
|
+
units: CompositeCatalogueRef[];
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** Input for `cnk.composites.create()`. */
|
|
1391
|
+
export interface CreateCompositeInput {
|
|
1392
|
+
name: string;
|
|
1393
|
+
/**
|
|
1394
|
+
* The tag identifier. Lowercased, and restricted to the same characters as
|
|
1395
|
+
* an `agentKey` — but validated harder than one, because it may not collide
|
|
1396
|
+
* with a field the ingest schemas already own (`customerId`, `usage`,
|
|
1397
|
+
* `agentKey`, `runId`, `member`, `custom`, `composite`, …). A colliding
|
|
1398
|
+
* `refId` is refused at create time rather than silently ignored at ingest.
|
|
1399
|
+
*/
|
|
1400
|
+
refId: string;
|
|
1401
|
+
/** USD per occurrence once a plan sells it. Required — an unpriced composite
|
|
1402
|
+
* put on a plan would bill 0, which is a money bug that is hard to see. */
|
|
1403
|
+
price: number;
|
|
1404
|
+
description?: string | null;
|
|
1405
|
+
/**
|
|
1406
|
+
* What the composite wraps. At least one id across the three lists is
|
|
1407
|
+
* REQUIRED: a composite restricted to nothing would accept every signal in
|
|
1408
|
+
* the org. Get the ids from `credits.list()`, `outcomes.list()` and
|
|
1409
|
+
* `units.list()`.
|
|
1410
|
+
*/
|
|
1411
|
+
entitlements: {
|
|
1412
|
+
creditIds?: string[];
|
|
1413
|
+
outcomeIds?: string[];
|
|
1414
|
+
unitIds?: string[];
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* The result of `signals.completeComposite()`.
|
|
1420
|
+
*
|
|
1421
|
+
* `status` distinguishes the first close from a replay. Note that an EARLY
|
|
1422
|
+
* ping — one sent before every credit / outcome / unit the composite is
|
|
1423
|
+
* restricted to has reported under the same value — is not an error: the
|
|
1424
|
+
* server parks it and completes it automatically when the missing signal
|
|
1425
|
+
* lands. That case answers with no `status`, only a `rawId` to follow.
|
|
1426
|
+
*/
|
|
1427
|
+
export interface CompositeCompletionResult {
|
|
1428
|
+
/** How far the ping got. Never `"buffered"` — completions are always sent
|
|
1429
|
+
* immediately. `"recorded"` means the occurrence was closed inline. See
|
|
1430
|
+
* {@link TrackResult.status}. */
|
|
1431
|
+
delivery: Extract<SignalDelivery, "accepted" | "recorded">;
|
|
1432
|
+
/** True when the ping was accepted but not yet applied (`202`). Prefer
|
|
1433
|
+
* {@link CompositeCompletionResult.delivery}. */
|
|
1434
|
+
queued: boolean;
|
|
1435
|
+
/** `COMPLETED` on the first close, `ALREADY_COMPLETED` on a replay. Absent
|
|
1436
|
+
* when the ping was queued or parked. Distinct from `delivery`: this is the
|
|
1437
|
+
* server's verdict on the OCCURRENCE, that is how far the REQUEST got. */
|
|
1438
|
+
status?: "COMPLETED" | "ALREADY_COMPLETED";
|
|
1439
|
+
/** The composite that was closed. Null on the replay of a historical row
|
|
1440
|
+
* whose occurrence can no longer be read back. */
|
|
1441
|
+
composite?: { id: string; name: string; refId: string } | null;
|
|
1442
|
+
/** The correlation value whose occurrence was closed. */
|
|
1443
|
+
value?: string | null;
|
|
1444
|
+
/** The durable-queue message id, present only on a `202`. */
|
|
1445
|
+
messageId?: string;
|
|
1446
|
+
/** The durable inbox id — see {@link TrackResult.rawId}. */
|
|
1447
|
+
rawId?: string;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/** One serialized unit-consumption event — the result of `signals.unit()` and
|
|
1451
|
+
* each element of `UnitUsageList.events`. */
|
|
1452
|
+
export interface SerializedUnitUsage {
|
|
1453
|
+
id: string;
|
|
1454
|
+
customerId: string;
|
|
1455
|
+
/** Display name of the unit. */
|
|
1456
|
+
unit: string;
|
|
1457
|
+
/** The unit's stable agent key; `null` if the unit was deleted. */
|
|
1458
|
+
agentKey: string | null;
|
|
1459
|
+
billingMode: BillingMode;
|
|
1460
|
+
/** Remaining ADVANCE allowance after this event; `null` for ARREAR. */
|
|
1461
|
+
remaining: number | null;
|
|
1462
|
+
customerCost: number;
|
|
1463
|
+
member: string | null;
|
|
1464
|
+
createdAt: string;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
export interface UnitUsageRow {
|
|
1468
|
+
unit: string;
|
|
1469
|
+
/** The unit's stable agent key — what to send to report against it. */
|
|
1470
|
+
agentKey: string | null;
|
|
1471
|
+
billingMode: BillingMode;
|
|
1472
|
+
granted?: number;
|
|
1473
|
+
used: number;
|
|
1474
|
+
remaining?: number;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
export interface UnitUsageList {
|
|
1478
|
+
customer: { id: string; name: string };
|
|
1479
|
+
units: UnitUsageRow[];
|
|
1480
|
+
events: SerializedUnitUsage[];
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// ---------------------------------------------------------------------------
|
|
1484
|
+
// Webhooks (threshold alerts)
|
|
1485
|
+
// ---------------------------------------------------------------------------
|
|
1486
|
+
|
|
1487
|
+
export type ThresholdKind = "CREDIT" | "OUTCOME" | "WALLET";
|
|
1488
|
+
|
|
1489
|
+
export interface Webhook {
|
|
1490
|
+
id: string;
|
|
1491
|
+
kind: ThresholdKind;
|
|
1492
|
+
threshold: number;
|
|
1493
|
+
url: string;
|
|
1494
|
+
isActive: boolean;
|
|
1495
|
+
createdAt: string;
|
|
1496
|
+
updatedAt: string;
|
|
1497
|
+
createdByName: string | null;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
export interface UpsertWebhookInput {
|
|
1501
|
+
kind: ThresholdKind;
|
|
1502
|
+
/** Fire when the balance crosses this value. */
|
|
1503
|
+
threshold: number;
|
|
1504
|
+
/** http(s) URL to POST the alert to. */
|
|
1505
|
+
url: string;
|
|
1506
|
+
isActive?: boolean;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// ---------------------------------------------------------------------------
|
|
1510
|
+
// Config & hooks
|
|
1511
|
+
// ---------------------------------------------------------------------------
|
|
1512
|
+
|
|
1513
|
+
export type FetchLike = (
|
|
1514
|
+
input: string,
|
|
1515
|
+
init?: RequestInit,
|
|
1516
|
+
) => Promise<Response>;
|
|
1517
|
+
|
|
1518
|
+
export interface Logger {
|
|
1519
|
+
debug?: (...args: unknown[]) => void;
|
|
1520
|
+
warn?: (...args: unknown[]) => void;
|
|
1521
|
+
error?: (...args: unknown[]) => void;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Why a signal was dropped without being billed.
|
|
1526
|
+
*
|
|
1527
|
+
* `queue_full` — the local buffer hit `batch.maxQueueSize`; never sent.
|
|
1528
|
+
* `closed` — tracked after `close()`; never sent.
|
|
1529
|
+
* `send_failed` — sent, and permanently rejected or unreachable. This is the
|
|
1530
|
+
* only reason that also fires `onError`, which carries the
|
|
1531
|
+
* error (and its `idempotencyKey`, for a safe replay).
|
|
1532
|
+
*/
|
|
1533
|
+
export type DropReason = "queue_full" | "closed" | "send_failed";
|
|
1534
|
+
|
|
1535
|
+
export interface ClockNextConfig {
|
|
1536
|
+
/** The org API key (`cnk_…`). Required. */
|
|
1537
|
+
apiKey: string;
|
|
1538
|
+
/** API base URL. Defaults to https://payments.clocknext.com */
|
|
1539
|
+
baseUrl?: string;
|
|
1540
|
+
/**
|
|
1541
|
+
* "async" (default) buffers signals and flushes in the background.
|
|
1542
|
+
* "sync" awaits every send inside `track()`.
|
|
1543
|
+
*/
|
|
1544
|
+
mode?: "async" | "sync";
|
|
1545
|
+
/** Per-request timeout (ms). Default 10_000. */
|
|
1546
|
+
timeoutMs?: number;
|
|
1547
|
+
/** Background batching config (async mode). */
|
|
1548
|
+
batch?: {
|
|
1549
|
+
/** Flush when this many signals are buffered. Default 20. */
|
|
1550
|
+
maxSize?: number;
|
|
1551
|
+
/** Flush at least this often (ms). Default 2000. */
|
|
1552
|
+
maxIntervalMs?: number;
|
|
1553
|
+
/** Max concurrent in-flight sends per flush. Default 5. */
|
|
1554
|
+
maxConcurrency?: number;
|
|
1555
|
+
/** Drop new signals once the buffer reaches this size. Default 10_000. */
|
|
1556
|
+
maxQueueSize?: number;
|
|
1557
|
+
};
|
|
1558
|
+
/** Retry policy for transient failures. */
|
|
1559
|
+
retry?: {
|
|
1560
|
+
/** Total attempts including the first. Default 5. */
|
|
1561
|
+
maxAttempts?: number;
|
|
1562
|
+
/** Base backoff (ms). Default 200. */
|
|
1563
|
+
baseDelayMs?: number;
|
|
1564
|
+
/** Max backoff (ms). Default 10_000. */
|
|
1565
|
+
maxDelayMs?: number;
|
|
1566
|
+
};
|
|
1567
|
+
/** Injectable fetch (defaults to global fetch). */
|
|
1568
|
+
fetch?: FetchLike;
|
|
1569
|
+
logger?: Logger;
|
|
1570
|
+
/** Called when a signal permanently fails (after retries / non-retryable). */
|
|
1571
|
+
onError?: (error: unknown, signal?: Signal) => void;
|
|
1572
|
+
/** Called after each successful background flush with the sent count. */
|
|
1573
|
+
onFlush?: (sentCount: number) => void;
|
|
1574
|
+
/** Called before each retry attempt. */
|
|
1575
|
+
onRetry?: (info: { attempt: number; delayMs: number; error: unknown }) => void;
|
|
1576
|
+
/** Called when a signal is dropped without being sent. */
|
|
1577
|
+
onDrop?: (signal: Signal, reason: DropReason) => void;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/** Fully-resolved config with defaults applied. */
|
|
1581
|
+
export interface ResolvedConfig {
|
|
1582
|
+
apiKey: string;
|
|
1583
|
+
baseUrl: string;
|
|
1584
|
+
mode: "async" | "sync";
|
|
1585
|
+
timeoutMs: number;
|
|
1586
|
+
batch: {
|
|
1587
|
+
maxSize: number;
|
|
1588
|
+
maxIntervalMs: number;
|
|
1589
|
+
maxConcurrency: number;
|
|
1590
|
+
maxQueueSize: number;
|
|
1591
|
+
};
|
|
1592
|
+
retry: { maxAttempts: number; baseDelayMs: number; maxDelayMs: number };
|
|
1593
|
+
fetch: FetchLike;
|
|
1594
|
+
logger: Logger;
|
|
1595
|
+
onError?: (error: unknown, signal?: Signal) => void;
|
|
1596
|
+
onFlush?: (sentCount: number) => void;
|
|
1597
|
+
onRetry?: (info: { attempt: number; delayMs: number; error: unknown }) => void;
|
|
1598
|
+
onDrop?: (signal: Signal, reason: DropReason) => void;
|
|
1599
|
+
}
|