@clocknext/sdk 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import type { Transport } from "../http.js";
2
+ import { newKey } from "../idempotency.js";
2
3
  import type {
3
4
  AddMemberInput,
5
+ AdjustResult,
4
6
  ApiCustomer,
5
7
  CompositeAdjustInput,
6
8
  CreateCustomerInput,
@@ -14,19 +16,18 @@ import type {
14
16
  MemberProfile,
15
17
  OutcomeAdjustInput,
16
18
  OutcomeBalance,
19
+ RecurringGrantInput,
20
+ RecurringGrantResult,
17
21
  RemovedMember,
18
22
  UnitAdjustInput,
19
23
  UnitBalance,
20
24
  UpdateMemberInput,
21
- UsageList,
22
- WalletEntryInput,
23
- WalletList,
24
- WalletTransaction,
25
+ WalletAdjustInput,
25
26
  } from "../types.js";
26
27
 
27
28
  /**
28
29
  * The `customers` resource — full CRUD over the org's customers plus the
29
- * read-only insight endpoints (usage / balances / plan). Every call is scoped
30
+ * read-only insight endpoints (balances / plan). Every call is scoped
30
31
  * to the API key's organisation server-side; a customer in another org
31
32
  * resolves as a NotFoundError.
32
33
  */
@@ -94,6 +95,42 @@ export class Customers {
94
95
  return res.customer;
95
96
  }
96
97
 
98
+ /**
99
+ * Fetch one customer by YOUR id for them (its `externalId`), instead of
100
+ * ours. Same result as {@link get}. `GET /api/v1/customers/ext/:externalId`.
101
+ * An unknown external id is a `NotFoundError`; only active customers match.
102
+ */
103
+ async getByExternalId(externalId: string): Promise<ApiCustomer> {
104
+ const res = await this.transport.request<{ customer: ApiCustomer }>({
105
+ method: "GET",
106
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}`,
107
+ });
108
+ return res.customer;
109
+ }
110
+
111
+ /** Update a customer (partial), addressed by its `externalId`. Same body as
112
+ * {@link update}. `PATCH /api/v1/customers/ext/:externalId`. */
113
+ async updateByExternalId(
114
+ externalId: string,
115
+ input: CustomerProfileInput,
116
+ ): Promise<ApiCustomer> {
117
+ const res = await this.transport.request<{ customer: ApiCustomer }>({
118
+ method: "PATCH",
119
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}`,
120
+ body: input,
121
+ });
122
+ return res.customer;
123
+ }
124
+
125
+ /** {@link delete}, addressing the customer by its `externalId`.
126
+ * `DELETE /api/v1/customers/ext/:externalId`. */
127
+ async deleteByExternalId(externalId: string): Promise<void> {
128
+ await this.transport.request({
129
+ method: "DELETE",
130
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}`,
131
+ });
132
+ }
133
+
97
134
  /** Delete a customer (members, invitations, usage logs cascade). */
98
135
  async delete(id: string): Promise<void> {
99
136
  await this.transport.request({
@@ -102,21 +139,20 @@ export class Customers {
102
139
  });
103
140
  }
104
141
 
105
- /** A customer's usage history. `GET /api/v1/usage?customerId=`. */
106
- async usage(id: string, params: { limit?: number } = {}): Promise<UsageList> {
107
- const res = await this.transport.request<UsageList>({
142
+ /** Wallet / credit / outcome / unit balances. `GET …/:id/balances`. */
143
+ balances(id: string): Promise<CustomerBalances> {
144
+ return this.transport.request<CustomerBalances>({
108
145
  method: "GET",
109
- path: `/api/v1/usage`,
110
- query: { customerId: id, limit: params.limit },
146
+ path: `/api/v1/customers/${encodeURIComponent(id)}/balances`,
111
147
  });
112
- return { customer: res.customer, totals: res.totals, logs: res.logs };
113
148
  }
114
149
 
115
- /** Wallet / credit / outcome / unit balances. `GET …/:id/balances`. */
116
- balances(id: string): Promise<CustomerBalances> {
150
+ /** {@link balances}, addressing the customer by its `externalId`.
151
+ * `GET /api/v1/customers/ext/:externalId/balances`. */
152
+ balancesByExternalId(externalId: string): Promise<CustomerBalances> {
117
153
  return this.transport.request<CustomerBalances>({
118
154
  method: "GET",
119
- path: `/api/v1/customers/${encodeURIComponent(id)}/balances`,
155
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}/balances`,
120
156
  });
121
157
  }
122
158
 
@@ -183,6 +219,15 @@ export class Customers {
183
219
  });
184
220
  }
185
221
 
222
+ /** {@link plan}, addressing the customer by its `externalId`.
223
+ * `GET /api/v1/customers/ext/:externalId/plan`. */
224
+ planByExternalId(externalId: string): Promise<CustomerPlan> {
225
+ return this.transport.request<CustomerPlan>({
226
+ method: "GET",
227
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}/plan`,
228
+ });
229
+ }
230
+
186
231
  // --- Members -------------------------------------------------------------
187
232
 
188
233
  /** List a customer's members. `GET …/:id/members`. */
@@ -227,95 +272,169 @@ export class Customers {
227
272
  return res.member;
228
273
  }
229
274
 
230
- // --- Wallet & billing config --------------------------------------------
275
+ // --- Billing config -----------------------------------------------------
231
276
 
232
- /** The customer's wallet balance + transactions. `GET …/:id/wallet`. */
233
- wallet(id: string, params: { limit?: number } = {}): Promise<WalletList> {
234
- return this.transport.request<WalletList>({
235
- method: "GET",
236
- path: `/api/v1/customers/${encodeURIComponent(id)}/wallet`,
237
- query: { limit: params.limit },
238
- });
277
+ // Note: a customer's currency is set through `update(id, { currencyCode })`
278
+ // (PATCH …/:id) — there is no separate currency endpoint. Credit-type
279
+ // assignment has no public API.
280
+
281
+ // --- Balance adjustments -------------------------------------------------
282
+
283
+ /** Grant or claw back a credit balance. The credit must be a prepaid part of
284
+ * the customer's current plan. Pass `idempotencyKey` to make it safe to
285
+ * retry. `POST …/:id/credits/adjust`. */
286
+ adjustCredit(id: string, input: CreditAdjustInput): Promise<AdjustResult> {
287
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/credits/adjust`, input);
288
+ }
289
+
290
+ /** Grant or claw back an outcome balance. The outcome must be a prepaid part
291
+ * of the customer's current plan. `POST …/:id/outcomes/adjust`. */
292
+ adjustOutcome(id: string, input: OutcomeAdjustInput): Promise<AdjustResult> {
293
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/outcomes/adjust`, input);
294
+ }
295
+
296
+ /** Grant or claw back a unit balance. The unit must be a prepaid part of the
297
+ * customer's current plan. `POST …/:id/units/adjust`. */
298
+ adjustUnit(id: string, input: UnitAdjustInput): Promise<AdjustResult> {
299
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/units/adjust`, input);
300
+ }
301
+
302
+ /** Grant or claw back slots on a customer's ADVANCE composite pool for the
303
+ * current billing cycle. `remaining` is the resulting slot count.
304
+ * `POST …/:id/composites/adjust`. */
305
+ adjustComposite(id: string, input: CompositeAdjustInput): Promise<AdjustResult> {
306
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/composites/adjust`, input);
239
307
  }
240
308
 
241
309
  /**
242
- * Record a manual wallet entry (top-up or deduction). `amount` is always
243
- * positive; the sign comes from `type`. `POST …/:id/wallet`.
310
+ * Top up (positive `delta`) or draw down (negative `delta`) a customer's
311
+ * wallet, in USD — allowed whatever the customer's plan. `remaining` is the
312
+ * resulting USD balance. No `agentKey` — a customer has exactly one wallet.
313
+ * Pass `pool: "RESERVED"` to move the Reserved Wallet instead (then
314
+ * `remaining` is its balance). `POST …/:id/wallet/adjust`.
244
315
  */
245
- async addWalletEntry(
246
- id: string,
247
- input: WalletEntryInput,
248
- ): Promise<{ transaction: WalletTransaction; balanceUsd: number }> {
249
- return this.transport.request<{
250
- transaction: WalletTransaction;
251
- balanceUsd: number;
252
- }>({
253
- method: "POST",
254
- path: `/api/v1/customers/${encodeURIComponent(id)}/wallet`,
255
- body: input,
256
- });
316
+ adjustWallet(id: string, input: WalletAdjustInput): Promise<AdjustResult> {
317
+ return this.adjust(`/api/v1/customers/${encodeURIComponent(id)}/wallet/adjust`, input);
257
318
  }
258
319
 
259
- // Note: a customer's currency is set through `update(id, { currencyCode })`
260
- // (PATCH …/:id) — there is no separate currency endpoint. Credit-type
261
- // assignment has no public API.
320
+ /** {@link adjustCredit}, addressing the customer by its `externalId` (`id`
321
+ * here). `POST /api/v1/customers/ext/:externalId/credits/adjust`. */
322
+ adjustCreditByExternalId(id: string, input: CreditAdjustInput): Promise<AdjustResult> {
323
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/credits/adjust`, input);
324
+ }
262
325
 
263
- // --- Balance adjustments -------------------------------------------------
326
+ /** {@link adjustOutcome}, addressing the customer by its `externalId` (`id`
327
+ * here). `POST /api/v1/customers/ext/:externalId/outcomes/adjust`. */
328
+ adjustOutcomeByExternalId(id: string, input: OutcomeAdjustInput): Promise<AdjustResult> {
329
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/outcomes/adjust`, input);
330
+ }
264
331
 
265
- /** Grant or claw back a credit balance. Returns the remaining balance.
266
- * `POST …/:id/credits/adjust`. */
267
- async adjustCredit(
268
- id: string,
269
- input: CreditAdjustInput,
270
- ): Promise<{ remaining: number }> {
271
- const res = await this.transport.request<{ remaining: number }>({
272
- method: "POST",
273
- path: `/api/v1/customers/${encodeURIComponent(id)}/credits/adjust`,
274
- body: input,
275
- });
276
- return { remaining: res.remaining };
332
+ /** {@link adjustUnit}, addressing the customer by its `externalId` (`id`
333
+ * here). `POST /api/v1/customers/ext/:externalId/units/adjust`. */
334
+ adjustUnitByExternalId(id: string, input: UnitAdjustInput): Promise<AdjustResult> {
335
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/units/adjust`, input);
277
336
  }
278
337
 
279
- /** Grant or claw back an outcome balance. Returns the remaining balance. */
280
- async adjustOutcome(
281
- id: string,
282
- input: OutcomeAdjustInput,
283
- ): Promise<{ remaining: number }> {
284
- const res = await this.transport.request<{ remaining: number }>({
285
- method: "POST",
286
- path: `/api/v1/customers/${encodeURIComponent(id)}/outcomes/adjust`,
287
- body: input,
288
- });
289
- return { remaining: res.remaining };
338
+ /** {@link adjustComposite}, addressing the customer by its `externalId`
339
+ * (`id` here). `POST /api/v1/customers/ext/:externalId/composites/adjust`. */
340
+ adjustCompositeByExternalId(id: string, input: CompositeAdjustInput): Promise<AdjustResult> {
341
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/composites/adjust`, input);
290
342
  }
291
343
 
292
- /** Grant or claw back a unit balance. Returns the remaining balance. */
293
- async adjustUnit(
294
- id: string,
295
- input: UnitAdjustInput,
296
- ): Promise<{ remaining: number }> {
297
- const res = await this.transport.request<{ remaining: number }>({
344
+ /** {@link adjustWallet}, addressing the customer by its `externalId` (`id`
345
+ * here). `POST /api/v1/customers/ext/:externalId/wallet/adjust`. */
346
+ adjustWalletByExternalId(id: string, input: WalletAdjustInput): Promise<AdjustResult> {
347
+ return this.adjust(`/api/v1/customers/ext/${encodeURIComponent(id)}/wallet/adjust`, input);
348
+ }
349
+
350
+ /**
351
+ * The one body behind all ten adjust calls.
352
+ *
353
+ * Every adjustment carries an idempotency key — the caller's, or one
354
+ * generated here ONCE per call, exactly like signals. The server
355
+ * deduplicates on it, so a transient failure is safe to retry: the same key
356
+ * rides every attempt, and a retry of one that already landed comes back as
357
+ * `replayed: true` instead of adjusting again. The key is also copied onto
358
+ * any thrown error (`error.idempotencyKey`) so the caller can replay safely.
359
+ */
360
+ private async adjust(
361
+ path: string,
362
+ input:
363
+ | CreditAdjustInput
364
+ | OutcomeAdjustInput
365
+ | UnitAdjustInput
366
+ | CompositeAdjustInput
367
+ | WalletAdjustInput,
368
+ ): Promise<AdjustResult> {
369
+ const idempotencyKey = input.idempotencyKey ?? newKey();
370
+ const res = await this.transport.request<{
371
+ remaining: number;
372
+ replayed?: boolean;
373
+ adjustedAt?: string;
374
+ customerArchived?: boolean;
375
+ }>({
298
376
  method: "POST",
299
- path: `/api/v1/customers/${encodeURIComponent(id)}/units/adjust`,
300
- body: input,
377
+ path,
378
+ idempotencyKey,
379
+ body: { ...input, idempotencyKey },
380
+ retry: true,
301
381
  });
302
- return { remaining: res.remaining };
382
+
383
+ const result: AdjustResult = {
384
+ remaining: res.remaining,
385
+ replayed: res.replayed === true,
386
+ customerArchived: res.customerArchived === true,
387
+ };
388
+ if (result.replayed && typeof res.adjustedAt === "string") {
389
+ result.adjustedAt = res.adjustedAt;
390
+ }
391
+ return result;
303
392
  }
304
393
 
394
+ // --- Recurring grants ----------------------------------------------------
395
+
305
396
  /**
306
- * Grant or claw back slots on a customer's ADVANCE composite pool for the
307
- * current billing cycle. Returns the resulting `remaining` slot count.
308
- * `POST …/:id/composites/adjust`.
397
+ * Change how much a customer is granted every billing cycle (wallet top-up,
398
+ * credits, outcomes, units or a composite pool). Any increase or decrease
399
+ * applies to their current balance immediately. `PUT …/:id/recurring-grant`.
309
400
  */
310
- async adjustComposite(
401
+ async updateRecurringGrant(
311
402
  id: string,
312
- input: CompositeAdjustInput,
313
- ): Promise<{ remaining: number }> {
314
- const res = await this.transport.request<{ remaining: number }>({
315
- method: "POST",
316
- path: `/api/v1/customers/${encodeURIComponent(id)}/composites/adjust`,
403
+ input: RecurringGrantInput,
404
+ ): Promise<RecurringGrantResult> {
405
+ const res = await this.transport.request<
406
+ RecurringGrantResult & { updated: true }
407
+ >({
408
+ method: "PUT",
409
+ path: `/api/v1/customers/${encodeURIComponent(id)}/recurring-grant`,
410
+ body: input,
411
+ });
412
+ return toRecurringGrantResult(res);
413
+ }
414
+
415
+ /** {@link updateRecurringGrant}, addressing the customer by its
416
+ * `externalId`. `PUT /api/v1/customers/ext/:externalId/recurring-grant`. */
417
+ async updateRecurringGrantByExternalId(
418
+ externalId: string,
419
+ input: RecurringGrantInput,
420
+ ): Promise<RecurringGrantResult> {
421
+ const res = await this.transport.request<
422
+ RecurringGrantResult & { updated: true }
423
+ >({
424
+ method: "PUT",
425
+ path: `/api/v1/customers/ext/${encodeURIComponent(externalId)}/recurring-grant`,
317
426
  body: input,
318
427
  });
319
- return { remaining: res.remaining };
428
+ return toRecurringGrantResult(res);
320
429
  }
321
430
  }
431
+
432
+ /** Drop the response's `updated: true` marker, keeping `balanceUsd` only when
433
+ * the server sent it (wallet grants). */
434
+ function toRecurringGrantResult(res: RecurringGrantResult): RecurringGrantResult {
435
+ return {
436
+ kind: res.kind,
437
+ quantity: res.quantity,
438
+ ...(res.balanceUsd !== undefined ? { balanceUsd: res.balanceUsd } : {}),
439
+ };
440
+ }
@@ -39,7 +39,7 @@ export class Invoices {
39
39
  }
40
40
 
41
41
  /**
42
- * Mint a hosted pay link for an OPEN invoice. PAID/VOID invoices are rejected
42
+ * Mint a hosted pay link for an OPEN invoice. PAID/VOID/AUTO_CLOSED invoices are rejected
43
43
  * with a `ConflictError`. `GET /api/v1/invoices/:id/pay-link`.
44
44
  */
45
45
  payLink(id: string): Promise<PayLink> {
@@ -1,16 +1,11 @@
1
1
  import { activeQuery, filterByActive } from "../filters.js";
2
2
  import type { Transport } from "../http.js";
3
- import type {
4
- CreatePlanInput,
5
- Plan,
6
- PlanDetail,
7
- UpdatePlanInput,
8
- } from "../types.js";
3
+ import type { Plan } from "../types.js";
9
4
 
10
5
  /**
11
- * The `plans` resource — define and manage the billing plans customers
12
- * subscribe to. Each plan is a set of entitlement components (wallet / credit /
13
- * outcome / unit / flat) billed on a cycle. `/api/v1/plans`.
6
+ * The `plans` resource — read the billing plans customers can subscribe to.
7
+ * Each plan is a set of entitlement components (wallet / credit / outcome /
8
+ * unit / flat) billed on a cycle. `/api/v1/plans`.
14
9
  */
15
10
  export class Plans {
16
11
  constructor(private readonly transport: Transport) {}
@@ -26,51 +21,4 @@ export class Plans {
26
21
  });
27
22
  return filterByActive(res.plans, params.active);
28
23
  }
29
-
30
- /** Create a plan. `POST /api/v1/plans`. */
31
- async create(input: CreatePlanInput): Promise<Plan> {
32
- const res = await this.transport.request<{ plan: Plan }>({
33
- method: "POST",
34
- path: "/api/v1/plans",
35
- body: input,
36
- });
37
- return res.plan;
38
- }
39
-
40
- /** Fetch one plan with aggregate subscriber/invoice stats. `GET /api/v1/plans/:id`. */
41
- async get(id: string): Promise<PlanDetail> {
42
- const res = await this.transport.request<{ plan: PlanDetail }>({
43
- method: "GET",
44
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
45
- });
46
- return res.plan;
47
- }
48
-
49
- /** Replace a plan's definition (components + settings). `PATCH /api/v1/plans/:id`. */
50
- async update(id: string, input: UpdatePlanInput): Promise<Plan> {
51
- const res = await this.transport.request<{ plan: Plan }>({
52
- method: "PATCH",
53
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
54
- body: input,
55
- });
56
- return res.plan;
57
- }
58
-
59
- /** Activate or deactivate a plan without rewriting it. */
60
- async setActive(id: string, isActive: boolean): Promise<Plan> {
61
- const res = await this.transport.request<{ plan: Plan }>({
62
- method: "PATCH",
63
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
64
- body: { isActive },
65
- });
66
- return res.plan;
67
- }
68
-
69
- /** Delete a plan. Refused once any customer has purchased it. */
70
- async delete(id: string): Promise<void> {
71
- await this.transport.request({
72
- method: "DELETE",
73
- path: `/api/v1/plans/${encodeURIComponent(id)}`,
74
- });
75
- }
76
24
  }
@@ -1,5 +1,6 @@
1
1
  import type { Transport } from "../http.js";
2
2
  import type { Flusher } from "../flusher.js";
3
+ import { customerRefToWire } from "../customer-ref.js";
3
4
  import { newKey } from "../idempotency.js";
4
5
  import type {
5
6
  CompositeCompletionInput,
@@ -15,8 +16,8 @@ import type {
15
16
  TrackResult,
16
17
  UnitInput,
17
18
  UnitTrackResult,
18
- UnitUsageList,
19
- UsageList,
19
+ UsageLogListParams,
20
+ UsageLogPage,
20
21
  WalletInput,
21
22
  } from "../types.js";
22
23
 
@@ -44,6 +45,7 @@ const SIGNAL_PATHS: Record<SignalType, string> = {
44
45
  const UNIT_PATH = "/api/v1/signal/unit";
45
46
  const COMPOSITE_COMPLETE_PATH = "/api/v1/signal/composite/complete";
46
47
  const OUTCOME_COMPLETE_PATH = "/api/v1/signal/outcome/complete";
48
+ const USAGE_LOG_PATH = "/api/v1/signal/usage-log";
47
49
 
48
50
  /**
49
51
  * Builds the request body for a typed Signal.
@@ -65,7 +67,7 @@ const OUTCOME_COMPLETE_PATH = "/api/v1/signal/outcome/complete";
65
67
  export function signalToWire(signal: Signal): Record<string, unknown> {
66
68
  const t = signal.tokens;
67
69
  const body: Record<string, unknown> = {
68
- customerId: signal.customerId,
70
+ ...customerRefToWire(signal),
69
71
  // `usage` is required on all three model-priced kinds. input/output are
70
72
  // always sent; `cacheTokens` only when provided (the server defaults 0).
71
73
  usage: {
@@ -279,9 +281,8 @@ export class Signals {
279
281
  path: OUTCOME_COMPLETE_PATH,
280
282
  idempotencyKey,
281
283
  body: {
282
- customerId: input.customerId,
284
+ ...customerRefToWire(input),
283
285
  runId: input.runId,
284
- complete: true,
285
286
  ...(input.member ? { member: input.member } : {}),
286
287
  idempotencyKey,
287
288
  },
@@ -297,27 +298,6 @@ export class Signals {
297
298
  };
298
299
  }
299
300
 
300
- /**
301
- * Recent usage logs + totals for a customer. `GET /api/v1/usage`.
302
- *
303
- * Still the legacy path, deliberately. The `/api/v1/signal/*` family is
304
- * POST-only — it replaced how usage is INGESTED, not how it is read — so
305
- * `GET /api/v1/usage` has no successor and is not deprecated. It is also the
306
- * only way to prove a signal landed, which matters more now that ingestion
307
- * answers `202` for everything.
308
- */
309
- async list(params: {
310
- customerId: string;
311
- limit?: number;
312
- }): Promise<UsageList> {
313
- const res = await this.transport.request<UsageList>({
314
- method: "GET",
315
- path: "/api/v1/usage",
316
- query: { customerId: params.customerId, limit: params.limit },
317
- });
318
- return { customer: res.customer, totals: res.totals, logs: res.logs };
319
- }
320
-
321
301
  /**
322
302
  * Record one unit-metered event (a fixed-price meter with no tokens),
323
303
  * addressed to a catalog unit by its `agentKey`. Sent immediately — unit
@@ -339,7 +319,7 @@ export class Signals {
339
319
  path: UNIT_PATH,
340
320
  idempotencyKey,
341
321
  body: {
342
- customerId: input.customerId,
322
+ ...customerRefToWire(input),
343
323
  agentKey: input.agentKey,
344
324
  ...(input.quantity != null ? { quantity: input.quantity } : {}),
345
325
  ...(input.member ? { member: input.member } : {}),
@@ -407,7 +387,7 @@ export class Signals {
407
387
  path: COMPOSITE_COMPLETE_PATH,
408
388
  idempotencyKey,
409
389
  body: {
410
- customerId: input.customerId,
390
+ ...customerRefToWire(input),
411
391
  composite: { [input.composite.ref]: input.composite.value },
412
392
  // Auto-assigned like the others. A retry here could not double-BILL
413
393
  // even without it — closing a closed occurrence is a server-side
@@ -430,19 +410,34 @@ export class Signals {
430
410
  }
431
411
 
432
412
  /**
433
- * A customer's unit balances + recent unit events. `GET /api/v1/units`.
413
+ * One customer's usage history, merged from every meter kind — credit,
414
+ * outcome, unit, wallet, and completed composite occurrences — into one
415
+ * feed sorted by time. `GET /api/v1/signal/usage-log`. Exactly the same
416
+ * rows the customer portal's own "Usage logs" table shows, so a caller can
417
+ * build against this instead of the portal UI.
434
418
  *
435
- * Legacy path for the same reason as {@link list}: the signal family is
436
- * POST-only, so the read side has no successor and is not deprecated.
419
+ * Cursor-paginated, NOT page-numbered: `limit` + `cursor` in, `nextCursor`
420
+ * out. Omit `cursor` for the first page; pass the previous page's
421
+ * `nextCursor` back to fetch the next one, and stop once it comes back
422
+ * `null`. Unlike offset pagination, the cost of a page never grows with how
423
+ * deep you've paged — each page only asks for rows strictly before the
424
+ * cursor's position.
425
+ *
426
+ * Read-only; unlike {@link track}/{@link unit}/etc., there is nothing to
427
+ * buffer or retry here.
437
428
  */
438
- unitUsage(params: {
439
- customerId: string;
440
- limit?: number;
441
- }): Promise<UnitUsageList> {
442
- return this.transport.request<UnitUsageList>({
429
+ usageLog(params: UsageLogListParams): Promise<UsageLogPage> {
430
+ return this.transport.request<UsageLogPage>({
443
431
  method: "GET",
444
- path: "/api/v1/units",
445
- query: { customerId: params.customerId, limit: params.limit },
432
+ path: USAGE_LOG_PATH,
433
+ query: {
434
+ customerId: params.customerId,
435
+ limit: params.limit,
436
+ cursor: params.cursor,
437
+ period: params.period,
438
+ from: params.from,
439
+ to: params.to,
440
+ },
446
441
  });
447
442
  }
448
443
  }