@clocknext/sdk 0.14.0 → 0.16.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,12 +1,14 @@
1
1
  /**
2
2
  * Basic usage: record a few signals from a long-lived Node process.
3
- * Run with: CLOCKNEXT_API_KEY=cnk_… npx tsx examples/node-basic.ts
4
3
  *
5
4
  * Note the discovery step first. `customerId`, `model` and `agentKey` are not
6
5
  * strings you can invent — they name rows that must already exist in your
7
6
  * workspace, and a signal naming one that does not is rejected. So this example
8
- * reads them out of the API rather than hard-coding placeholders that are
9
- * guaranteed to 422 on someone else's workspace.
7
+ * reads the customer and its agent keys out of the API rather than hard-coding
8
+ * placeholders that are guaranteed to 422 on someone else's workspace. The
9
+ * model id comes from Settings → Models in the dashboard:
10
+ *
11
+ * CLOCKNEXT_API_KEY=cnk_… CLOCKNEXT_MODEL=gpt-4o npx tsx examples/node-basic.ts
10
12
  */
11
13
  import { ClockNext } from "@clocknext/sdk";
12
14
 
@@ -18,34 +20,28 @@ const cnk = new ClockNext({
18
20
  });
19
21
 
20
22
  async function main() {
21
- // 0. Ground the integration. Check WHICH workspace this key points at before
22
- // sending anything — a live key bills for real.
23
- const { organization } = await cnk.workspace.me();
24
- console.log(`workspace: ${organization.name} (${organization.environment})`);
25
-
26
- // 1. Discover real ids instead of guessing them.
27
- const [models, credits, customers] = await Promise.all([
28
- cnk.workspace.models({ active: true }),
29
- cnk.credits.list({ active: true }),
30
- cnk.customers.list({ limit: 1 }),
31
- ]);
32
-
33
- const model = models[0];
34
- const credit = credits[0];
35
- const customer = customers.customers[0];
36
- if (!model || !credit || !customer) {
23
+ // 1. Discover real ids instead of guessing them. A customer's balances list
24
+ // every credit and outcome step its plan sells, each with its agentKey.
25
+ const model = process.env.CLOCKNEXT_MODEL;
26
+ const customer = (await cnk.customers.list({ limit: 1 })).customers[0];
27
+ if (!model || !customer) {
37
28
  throw new Error(
38
- "This workspace needs at least one enabled model, one credit type, and one customer before it can meter anything.",
29
+ "Set CLOCKNEXT_MODEL to a model enabled under Settings → Models, and create at least one customer, before metering anything.",
39
30
  );
40
31
  }
41
- console.log(`metering ${credit.agentKey} on ${model.modelId}`);
32
+ const balances = await cnk.customers.balances(customer.id);
33
+ const credit = balances.credits[0];
34
+ if (!credit) {
35
+ throw new Error("This customer's plan needs at least one credit before it can meter credit usage.");
36
+ }
37
+ console.log(`metering ${credit.agentKey} on ${model}`);
42
38
 
43
39
  // 2. Credit usage — metered against a credit by its stable `agentKey`, priced
44
40
  // by tokens. Optionally group it into a composite occurrence, billed once
45
41
  // on close.
46
42
  await cnk.signals.credit({
47
43
  customerId: customer.id,
48
- model: model.modelId,
44
+ model,
49
45
  agentKey: credit.agentKey,
50
46
  tokens: { input: 1200, output: 340, cache: 0 },
51
47
  custom: { feature: "summarize", tier: "pro" },
@@ -54,23 +50,19 @@ async function main() {
54
50
  // 3. Wallet usage (debits the customer's USD wallet at model cost).
55
51
  await cnk.signals.wallet({
56
52
  customerId: customer.id,
57
- model: model.modelId,
53
+ model,
58
54
  tokens: { input: 800, output: 5000 },
59
55
  });
60
56
 
61
57
  // 4. Outcome run — every step shares a `runId`; a separate, dedicated call
62
58
  // (`completeOutcome`) closes and bills the run — never a flag on a step.
63
- // `agentKey` is the OutcomeStep's key, which you read off
64
- // `cnk.outcomes.list()` the same way.
65
- // `outcomes.list()` returns catalogue rows WITHOUT their steps; the step
66
- // agent keys — which is what a signal reports against — are on the detail.
67
- const outcomeRow = (await cnk.outcomes.list({ active: true }))[0];
68
- const outcome = outcomeRow ? await cnk.outcomes.get(outcomeRow.id) : undefined;
59
+ // `agentKey` is the OutcomeStep's key, read off the same balances.
60
+ const outcome = balances.outcomes[0];
69
61
  if (outcome?.steps[0]) {
70
62
  const runId = `demo_run_${Date.now()}`;
71
63
  await cnk.signals.outcome({
72
64
  customerId: customer.id,
73
- model: model.modelId,
65
+ model,
74
66
  agentKey: outcome.steps[0].agentKey,
75
67
  runId,
76
68
  tokens: { input: 600, output: 150 },
@@ -83,8 +75,8 @@ async function main() {
83
75
  // asynchronous, so this — not the track() return value — is what proves a
84
76
  // signal actually landed.
85
77
  await cnk.flush();
86
- const usage = await cnk.signals.list({ customerId: customer.id, limit: 10 });
87
- console.log("recent logs:", usage.logs.length, "totals:", usage.totals);
78
+ const page = await cnk.signals.usageLog({ customerId: customer.id, limit: 10 });
79
+ console.log("recent logs:", page.usageLogs.length);
88
80
 
89
81
  await cnk.close();
90
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clocknext/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Official ClockNext SDK — record usage signals and manage plans, purchases, invoices, customers, and billing from your backend.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.ts CHANGED
@@ -12,8 +12,6 @@ import { Portal } from "./resources/portal.js";
12
12
  import { Purchases } from "./resources/purchases.js";
13
13
  import { Signals } from "./resources/signals.js";
14
14
  import { Units } from "./resources/units.js";
15
- import { Webhooks } from "./resources/webhooks.js";
16
- import { Workspace } from "./resources/workspace.js";
17
15
  import type { ClockNextConfig, ResolvedConfig } from "./types.js";
18
16
 
19
17
  /**
@@ -34,32 +32,28 @@ import type { ClockNextConfig, ResolvedConfig } from "./types.js";
34
32
  * ```
35
33
  */
36
34
  export class ClockNext {
37
- /** Record + read usage signals (credit / wallet / outcome / unit). */
35
+ /** Record usage signals (credit / wallet / outcome / unit) and read the usage log. */
38
36
  readonly signals: Signals;
39
- /** Manage customers, their members, wallet, and balances. */
37
+ /** Manage customers, their members, and balances. */
40
38
  readonly customers: Customers;
41
- /** Define and manage billing plans. */
39
+ /** Billing plans: list, get, create, update, archive, unarchive. */
42
40
  readonly plans: Plans;
41
+ /** Credits: list, get, create, update, archive, unarchive (no delete). */
42
+ readonly credits: Credits;
43
+ /** Outcomes: list, get, create, update, archive, unarchive (no delete). */
44
+ readonly outcomes: Outcomes;
45
+ /** Units: list, get, create, update, archive, unarchive (no delete). */
46
+ readonly units: Units;
47
+ /** Composites: list, get, create, update, archive, unarchive (no delete). */
48
+ readonly composites: Composites;
43
49
  /** Subscribe customers to plans (purchases). */
44
50
  readonly purchases: Purchases;
45
51
  /** Read invoices and mint hosted pay links. */
46
52
  readonly invoices: Invoices;
47
53
  /** Read payments (paid invoices). */
48
54
  readonly payments: Payments;
49
- /** Manage the credit-type catalog. */
50
- readonly credits: Credits;
51
- /** Manage the composite catalog — bundles billed as one thing. */
52
- readonly composites: Composites;
53
- /** Manage the outcome-type catalog. */
54
- readonly outcomes: Outcomes;
55
- /** Manage the unit-type catalog. */
56
- readonly units: Units;
57
- /** Configure threshold webhooks. */
58
- readonly webhooks: Webhooks;
59
55
  /** Mint customer-portal embed tokens. */
60
56
  readonly portal: Portal;
61
- /** Identify the workspace (sandbox/live) + list its models. `/me`, `/models`. */
62
- readonly workspace: Workspace;
63
57
 
64
58
  private readonly cfg: ResolvedConfig;
65
59
  private readonly flusher: Flusher;
@@ -78,16 +72,14 @@ export class ClockNext {
78
72
  this.signals = new Signals(transport, this.flusher, this.cfg);
79
73
  this.customers = new Customers(transport);
80
74
  this.plans = new Plans(transport);
81
- this.purchases = new Purchases(transport);
82
- this.invoices = new Invoices(transport);
83
- this.payments = new Payments(transport);
84
75
  this.credits = new Credits(transport);
85
- this.composites = new Composites(transport);
86
76
  this.outcomes = new Outcomes(transport);
87
77
  this.units = new Units(transport);
88
- this.webhooks = new Webhooks(transport);
78
+ this.composites = new Composites(transport);
79
+ this.purchases = new Purchases(transport);
80
+ this.invoices = new Invoices(transport);
81
+ this.payments = new Payments(transport);
89
82
  this.portal = new Portal(transport);
90
- this.workspace = new Workspace(transport);
91
83
  }
92
84
 
93
85
  /** Number of signals currently buffered (async mode). */
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The customer half of a request body: `customerId` and/or `externalId`,
3
+ * each only when set. Both are forwarded as given — the server applies the
4
+ * precedence (`customerId` wins), so the SDK never second-guesses it.
5
+ */
6
+ export function customerRefToWire(ref: {
7
+ customerId?: string;
8
+ externalId?: string;
9
+ }): { customerId?: string; externalId?: string } {
10
+ return {
11
+ ...(ref.customerId != null ? { customerId: ref.customerId } : {}),
12
+ ...(ref.externalId != null ? { externalId: ref.externalId } : {}),
13
+ };
14
+ }
package/src/errors.ts CHANGED
@@ -107,7 +107,7 @@ export class NetworkError extends ClockNextError {
107
107
  *
108
108
  * The server validates the WIRE body and names the offending path in its 400
109
109
  * (`usage.inputTokens: Expected number…`). But the SDK groups those fields
110
- * differently — `tokens: { input, output, cache }` — so the path it reports
110
+ * differently — `tokens: { input, output, cache, cacheWrite }` — so the path it reports
111
111
  * names something that does not exist anywhere in this SDK's API. A caller
112
112
  * (human or LLM) then searches their own code for `usage.inputTokens`, finds
113
113
  * nothing, and has no next move.
@@ -120,6 +120,7 @@ const FIELD_PATHS: ReadonlyArray<readonly [RegExp, string]> = [
120
120
  [/\busage\.inputTokens\b/g, "tokens.input"],
121
121
  [/\busage\.outputTokens\b/g, "tokens.output"],
122
122
  [/\busage\.cacheTokens\b/g, "tokens.cache"],
123
+ [/\busage\.cacheWriteTokens\b/g, "tokens.cacheWrite"],
123
124
  [/\busage\.model\b/g, "model"],
124
125
  // A missing/invalid `usage` block as a whole: the caller's equivalent is
125
126
  // `tokens` (plus `model`, which the SDK hoists to the top level).
@@ -154,7 +155,11 @@ export function translateFieldPaths(message: string): string {
154
155
  * toward it still classifies, never as the sole test.
155
156
  */
156
157
  const ALLOWANCE_MESSAGE =
157
- /\binsufficient\b|\bno allowance left\b|\bno slots left\b|\boutcomes remaining\b|\btop up the (?:allowance|balance)\b/i;
158
+ /\binsufficient\b|\bno allowance left\b|\bno slots left\b|\boutcomes remaining\b|\btop up the (?:allowance|balance)\b|\bbelow zero\b/i;
159
+
160
+ /** The adjust API's 422 for an idempotency key reused with a DIFFERENT
161
+ * request. A conflict, not a plan problem — and never worth retrying. */
162
+ const IDEMPOTENCY_KEY_REUSED = /\bidempotency key was already used\b/i;
158
163
 
159
164
  export function isAllowanceFailure(message: string, code?: string): boolean {
160
165
  if (code) return code === "ALLOWANCE_EXHAUSTED";
@@ -205,6 +210,9 @@ export function errorFromResponse(
205
210
  });
206
211
  }
207
212
  if (status === 422) {
213
+ if (IDEMPOTENCY_KEY_REUSED.test(raw)) {
214
+ return new ConflictError(raw, { ...opts, retryable: false });
215
+ }
208
216
  return isAllowanceFailure(raw, code)
209
217
  ? new AllowanceError(raw, opts)
210
218
  : new PlanError(raw, opts);
package/src/filters.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The `active` list filter, shared by every catalog resource.
2
+ * The `active` list filter, used by `plans.list()`.
3
3
  *
4
4
  * The API offers exactly ONE filter: `?active=true` narrows a list to active
5
5
  * rows. There is no inactive-only parameter — the server tests
package/src/index.ts CHANGED
@@ -47,7 +47,6 @@ export type {
47
47
  OutcomeCompletionInput,
48
48
  OutcomeCompletionResult,
49
49
  TrackResult,
50
- UsageList,
51
50
  SerializedUsageLog,
52
51
  UsageBalance,
53
52
  SerializedRuleApplication,
@@ -56,8 +55,6 @@ export type {
56
55
  UnitTrackResult,
57
56
  CompositeCompletionInput,
58
57
  CompositeCompletionResult,
59
- UnitUsageRow,
60
- UnitUsageList,
61
58
  SerializedUnitUsage,
62
59
  UsageLogPeriod,
63
60
  UsageLogListParams,
@@ -65,10 +62,8 @@ export type {
65
62
  UsageLogOutcomeAttribution,
66
63
  UsageLogEntry,
67
64
  UsageLogPage,
68
- // workspace
69
- WorkspaceIdentity,
70
- Model,
71
65
  // customers
66
+ CustomerRef,
72
67
  ApiCustomer,
73
68
  CustomerProfileInput,
74
69
  CreateCustomerInput,
@@ -77,6 +72,7 @@ export type {
77
72
  CreditBalance,
78
73
  OutcomeBalance,
79
74
  UnitBalance,
75
+ CompositeBalance,
80
76
  CustomerPlan,
81
77
  CustomerPlanDetails,
82
78
  // members
@@ -85,30 +81,44 @@ export type {
85
81
  RemovedMember,
86
82
  AddMemberInput,
87
83
  UpdateMemberInput,
88
- // wallet & balance adjustments
89
- WalletTransaction,
90
- WalletTransactionType,
91
- WalletTransactionSource,
92
- WalletEntitlementKind,
93
- WalletList,
94
- WalletEntryInput,
84
+ // balance adjustments
85
+ WalletAdjustInput,
86
+ AdjustResult,
87
+ WalletPool,
95
88
  CreditAdjustInput,
96
89
  OutcomeAdjustInput,
97
90
  UnitAdjustInput,
98
91
  CompositeAdjustInput,
92
+ RecurringGrantInput,
93
+ RecurringGrantResult,
99
94
  // plans
100
95
  Plan,
101
- PlanDetail,
102
- PlanComponent,
103
- PlanComponentInput,
104
- WalletComponentInput,
105
- FlatComponentInput,
106
- CreditComponentInput,
107
- OutcomeComponentInput,
108
- UnitComponentInput,
109
- CompositeComponentInput,
96
+ PlanEntitlement,
97
+ PlanEntitlementInput,
110
98
  CreatePlanInput,
111
99
  UpdatePlanInput,
100
+ // catalogue: credits, outcomes, units, composites
101
+ ModelBundleLine,
102
+ ModelBundleLineInput,
103
+ Credit,
104
+ CreateCreditInput,
105
+ UpdateCreditInput,
106
+ Outcome,
107
+ OutcomeStep,
108
+ OutcomeWithSteps,
109
+ OutcomeStepInput,
110
+ CreateOutcomeInput,
111
+ UpdateOutcomeInput,
112
+ UnitPricingType,
113
+ UnitTier,
114
+ Unit,
115
+ CreateUnitInput,
116
+ UpdateUnitInput,
117
+ CompositeItem,
118
+ Composite,
119
+ CreateCompositeInput,
120
+ UpdateCompositeInput,
121
+ PlanComponent,
112
122
  BillingCycle,
113
123
  PlanType,
114
124
  PlanKind,
@@ -132,42 +142,6 @@ export type {
132
142
  // payments
133
143
  PaymentRow,
134
144
  PaymentDetail,
135
- // cost analytics (credits / outcomes)
136
- CostPoint,
137
- CostStats,
138
- CreditAnalytics,
139
- OutcomeAnalytics,
140
- // credits catalog
141
- Credit,
142
- CreditDetail,
143
- CreateCreditInput,
144
- UpdateCreditInput,
145
- ModelBundleEntry,
146
- // outcomes catalog
147
- Outcome,
148
- OutcomeDetail,
149
- OutcomeStepInput,
150
- CreateOutcomeInput,
151
- UpdateOutcomeInput,
152
- // composites catalog
153
- Composite,
154
- CompositeCatalogueRef,
155
- CreateCompositeInput,
156
- // units catalog
157
- Unit,
158
- UnitDetail,
159
- UnitCustomerRow,
160
- UnitTier,
161
- UnitTierInput,
162
- UnitPricingType,
163
- CreateUnitInput,
164
- FlatUnitInput,
165
- TieredUnitInput,
166
- UpdateUnitInput,
167
- // webhooks
168
- Webhook,
169
- UpsertWebhookInput,
170
- ThresholdKind,
171
145
  // portal
172
146
  PortalTokenInput,
173
147
  PortalToken,
@@ -1,31 +1,24 @@
1
1
  import { activeQuery, filterByActive } from "../filters.js";
2
2
  import type { Transport } from "../http.js";
3
- import type { Composite, CreateCompositeInput } from "../types.js";
3
+ import type { Composite, CreateCompositeInput, UpdateCompositeInput } from "../types.js";
4
4
 
5
5
  /**
6
- * The `composites` resource — manage the org's composite *catalogue*: the
7
- * bundles of credits / outcomes / units that are grouped under one tag and
8
- * sold on a plan as a single thing. `/api/v1/composites`.
6
+ * The `composites` resource — several credits / outcomes / units grouped under
7
+ * one tag (`refId`) and billed as ONE thing per completed occurrence.
8
+ * `/api/v1/composites`.
9
9
  *
10
- * Two things live elsewhere, deliberately:
11
- * • Tagging a signal into a composite occurrence is `composite: { ref, value }`
12
- * on the signal itself (see `signals.credit()` / `.outcome()` / `.unit()`).
13
- * • Closing an occurrence — the moment it is billed — is
14
- * `signals.completeComposite()`.
10
+ * Creating a composite charges nobody: it only bills once a plan sells it as
11
+ * a `PRICING_METRIC` entitlement (`plans.create()` with its `compositeId`).
15
12
  *
16
- * This resource only defines what composites EXIST. Creating one prices it but
17
- * charges nobody: an invoice is built from plan components, never from a
18
- * catalogue entry, so a composite starts costing money only once a plan sells
19
- * it (`quantity × price`, frozen into the purchase at buy time).
13
+ * There is no `delete()` — the API never deletes catalogue items. Retire a
14
+ * composite with `archive()` (new signals stop resolving to it; its history is
15
+ * kept); `unarchive()` brings it back.
20
16
  */
21
17
  export class Composites {
22
18
  constructor(private readonly transport: Transport) {}
23
19
 
24
- /** List composites. Pass `{ active: true }` for only the ones a signal can
25
- * still be tagged with — the default list INCLUDES archived ones, which
26
- * keep every row they already own.
27
- * `{ active: false }` returns only archived ones; omitting it returns
28
- * both. See `activeQuery` — the API itself only supports `true`. */
20
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
21
+ * only archived ones; omit it for both. */
29
22
  async list(params: { active?: boolean } = {}): Promise<Composite[]> {
30
23
  const res = await this.transport.request<{ composites: Composite[] }>({
31
24
  method: "GET",
@@ -35,13 +28,55 @@ export class Composites {
35
28
  return filterByActive(res.composites, params.active);
36
29
  }
37
30
 
38
- /** Create a composite. `POST /api/v1/composites`. */
39
- async create(input: CreateCompositeInput): Promise<Composite> {
31
+ /** One composite's configuration. */
32
+ async get(id: string): Promise<Composite> {
40
33
  const res = await this.transport.request<{ composite: Composite }>({
34
+ method: "GET",
35
+ path: `/api/v1/composites/${encodeURIComponent(id)}`,
36
+ });
37
+ return res.composite;
38
+ }
39
+
40
+ /** Create one. It starts active. Not retried on failure — a retry could
41
+ * create a second row. */
42
+ async create(input: CreateCompositeInput): Promise<Composite> {
43
+ const res = await this.transport.request<{ created: true; composite: Composite }>({
41
44
  method: "POST",
42
45
  path: "/api/v1/composites",
43
46
  body: input,
44
47
  });
45
48
  return res.composite;
46
49
  }
50
+
51
+ /** PARTIAL update: only the fields you send change. `entitlements`, when
52
+ * sent, is the complete new set. Renaming `refId` cuts live traffic over at
53
+ * once — ship the new tag to your callers first. */
54
+ async update(id: string, input: UpdateCompositeInput): Promise<Composite> {
55
+ const res = await this.transport.request<{ updated: true; composite: Composite }>({
56
+ method: "PATCH",
57
+ path: `/api/v1/composites/${encodeURIComponent(id)}`,
58
+ body: input,
59
+ });
60
+ return res.composite;
61
+ }
62
+
63
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
64
+ async archive(id: string): Promise<Composite> {
65
+ const res = await this.transport.request<{ archived: true; composite: Composite }>({
66
+ method: "POST",
67
+ path: `/api/v1/composites/${encodeURIComponent(id)}/archive`,
68
+ retry: true,
69
+ });
70
+ return res.composite;
71
+ }
72
+
73
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
74
+ async unarchive(id: string): Promise<Composite> {
75
+ const res = await this.transport.request<{ unarchived: true; composite: Composite }>({
76
+ method: "POST",
77
+ path: `/api/v1/composites/${encodeURIComponent(id)}/unarchive`,
78
+ retry: true,
79
+ });
80
+ return res.composite;
81
+ }
47
82
  }
@@ -1,23 +1,24 @@
1
1
  import { activeQuery, filterByActive } from "../filters.js";
2
2
  import type { Transport } from "../http.js";
3
- import type {
4
- CreateCreditInput,
5
- Credit,
6
- CreditDetail,
7
- UpdateCreditInput,
8
- } from "../types.js";
3
+ import type { CreateCreditInput, Credit, UpdateCreditInput } from "../types.js";
9
4
 
10
5
  /**
11
- * The `credits` resource — manage the org's credit *types* (the catalog). A
12
- * credit's stable `agentKey` is what you pass to `signals.credit()` when
13
- * recording usage against it. `/api/v1/credits`.
6
+ * The `credits` resource — the token-metered credits your product draws down
7
+ * by `agentKey`. `/api/v1/credits`.
8
+ *
9
+ * Pricing is never typed in: `create()` takes a `modelBundle` (one line per
10
+ * model, each with `averageTokensPerLLMCall` and its input / output / cache /
11
+ * cacheWrite split) and the server works out `basePrice` and
12
+ * `pricePerCredit` from the models' live prices.
13
+ *
14
+ * There is no `delete()` — the API never deletes catalogue items. Retire a
15
+ * credit with `archive()`; `unarchive()` brings it back unchanged.
14
16
  */
15
17
  export class Credits {
16
18
  constructor(private readonly transport: Transport) {}
17
19
 
18
- /** List credit types. Pass `{ active: true }` to only return active ones.
19
- * `{ active: false }` returns only archived ones; omitting it returns
20
- * both. See `activeQuery` — the API itself only supports `true`. */
20
+ /** List them. `{ active: true }` returns only active ones, `{ active: false }`
21
+ * only archived ones; omit it for both. */
21
22
  async list(params: { active?: boolean } = {}): Promise<Credit[]> {
22
23
  const res = await this.transport.request<{ credits: Credit[] }>({
23
24
  method: "GET",
@@ -27,28 +28,30 @@ export class Credits {
27
28
  return filterByActive(res.credits, params.active);
28
29
  }
29
30
 
30
- /** Create a credit type; returns the created credit. `POST /api/v1/credits`. */
31
- async create(input: CreateCreditInput): Promise<Credit> {
31
+ /** One credit's configuration. */
32
+ async get(id: string): Promise<Credit> {
32
33
  const res = await this.transport.request<{ credit: Credit }>({
33
- method: "POST",
34
- path: "/api/v1/credits",
35
- body: input,
34
+ method: "GET",
35
+ path: `/api/v1/credits/${encodeURIComponent(id)}`,
36
36
  });
37
37
  return res.credit;
38
38
  }
39
39
 
40
- /** Fetch one credit type with usage stats. `GET /api/v1/credits/:id`. */
41
- async get(id: string): Promise<CreditDetail> {
42
- const res = await this.transport.request<{ credit: CreditDetail }>({
43
- method: "GET",
44
- path: `/api/v1/credits/${encodeURIComponent(id)}`,
40
+ /** Create one. It starts active. Not retried on failure — a retry could
41
+ * create a second row. */
42
+ async create(input: CreateCreditInput): Promise<Credit> {
43
+ const res = await this.transport.request<{ created: true; credit: Credit }>({
44
+ method: "POST",
45
+ path: "/api/v1/credits",
46
+ body: input,
45
47
  });
46
48
  return res.credit;
47
49
  }
48
50
 
49
- /** Update a credit type's definition; returns the updated credit. `PATCH /api/v1/credits/:id`. */
51
+ /** PARTIAL update: only the fields you send change. Send `modelBundle` to
52
+ * re-price; leave it out to keep the stored base price. */
50
53
  async update(id: string, input: UpdateCreditInput): Promise<Credit> {
51
- const res = await this.transport.request<{ credit: Credit }>({
54
+ const res = await this.transport.request<{ updated: true; credit: Credit }>({
52
55
  method: "PATCH",
53
56
  path: `/api/v1/credits/${encodeURIComponent(id)}`,
54
57
  body: input,
@@ -56,21 +59,23 @@ export class Credits {
56
59
  return res.credit;
57
60
  }
58
61
 
59
- /** Activate or deactivate a credit type. */
60
- async setActive(id: string, isActive: boolean): Promise<Credit> {
61
- const res = await this.transport.request<{ credit: Credit }>({
62
- method: "PATCH",
63
- path: `/api/v1/credits/${encodeURIComponent(id)}`,
64
- body: { isActive },
62
+ /** Retire it (`isActive` → false). Nothing is deleted. Safe to retry. */
63
+ async archive(id: string): Promise<Credit> {
64
+ const res = await this.transport.request<{ archived: true; credit: Credit }>({
65
+ method: "POST",
66
+ path: `/api/v1/credits/${encodeURIComponent(id)}/archive`,
67
+ retry: true,
65
68
  });
66
69
  return res.credit;
67
70
  }
68
71
 
69
- /** Delete a credit type. Refused while a customer is linked to it. */
70
- async delete(id: string): Promise<void> {
71
- await this.transport.request({
72
- method: "DELETE",
73
- path: `/api/v1/credits/${encodeURIComponent(id)}`,
72
+ /** Bring an archived one back (`isActive` → true). Safe to retry. */
73
+ async unarchive(id: string): Promise<Credit> {
74
+ const res = await this.transport.request<{ unarchived: true; credit: Credit }>({
75
+ method: "POST",
76
+ path: `/api/v1/credits/${encodeURIComponent(id)}/unarchive`,
77
+ retry: true,
74
78
  });
79
+ return res.credit;
75
80
  }
76
81
  }