@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/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
+ }