@tapi-dev/sdk 0.1.15 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,150 +10,266 @@ npm install @tapi-dev/sdk
10
10
 
11
11
  This package is ESM-first and works in runtimes with `fetch`, including modern Node.js and browser-like server runtimes.
12
12
 
13
- ## Developer Flow
14
-
15
- Tapi Studio is launched from the developer app repo. The repo's `.tapi/project.json`
16
- is the project binding, so one developer can work on multiple Tapi projects from
17
- different directories without a global Studio project picker.
18
-
19
- ```bash
20
- npm install @tapi-dev/sdk
21
- npx tapi init --project brokerage
22
- npx tapi studio
23
- ```
24
-
25
- `tapi studio` checks the required local `tapi-service`, installs it when needed,
26
- downloads the portable Studio server release when the channel manifest uses
27
- `installerKind: portable-server`, starts Studio locally, and opens the browser.
28
- If the channel still publishes a legacy NSIS desktop Studio manifest and no
29
- Studio executable is installed yet, `tapi studio` runs the installer first, then
30
- opens Studio. The normal developer path is the portable server launched by the
31
- CLI.
32
-
33
- Useful commands:
34
-
35
- ```bash
36
- npx tapi init --project brokerage
37
- npx tapi link --project brokerage
38
- npx tapi studio
39
- npx tapi service install --channel pilot
40
- npx tapi service status
41
- npx tapi apis generate
42
- npx tapi publish
43
- ```
44
-
45
- Local authoring files live in the app repo:
46
-
47
- ```text
48
- .tapi/project.json
49
- .tapi/sitemaps/<site>.json
50
- .tapi/apis/<site>/<api>.json
51
- .tapi/generated/catalog.json
52
- src/tapi.generated.ts
53
- ```
54
-
55
- Drafts are local. `tapi publish` uploads local sitemaps and API contracts, marks
56
- ready requests as published, and activates one immutable server release. Runtime
57
- SDK calls read the active release catalog, not mutable Studio drafts.
58
-
59
- ```bash
60
- TAPI_API_KEY=tapi_project_key npx tapi publish
61
- TAPI_API_KEY=tapi_project_key npx tapi apis generate
13
+ The CLI command is `tapi`. If your package manager only installed the SDK
14
+ locally and `tapi` is not on `PATH`, use `npx tapi` as a fallback.
15
+
16
+ ## Developer Flow
17
+
18
+ Tapi Studio is launched from the developer app repo. The repo's `.tapi/project.json`
19
+ is the project binding, so one developer can work on multiple Tapi projects from
20
+ different directories without a global Studio project picker.
21
+
22
+ ```bash
23
+ npm install @tapi-dev/sdk
24
+ tapi init --project brokerage
25
+ tapi studio
26
+ ```
27
+
28
+ `tapi studio` checks the required local `tapi-service`, installs it when needed,
29
+ downloads the portable Studio server release when the channel manifest uses
30
+ `installerKind: portable-server`, starts Studio locally, and opens the browser.
31
+ If the channel still publishes a legacy NSIS desktop Studio manifest and no
32
+ Studio executable is installed yet, `tapi studio` runs the installer first, then
33
+ opens Studio. The normal developer path is the portable server launched by the
34
+ CLI.
35
+
36
+ By default, Studio downloads and extracted portable server releases live under
37
+ the user's local app data directory. On Windows that is usually
38
+ `%LOCALAPPDATA%\Tapi\Studio`. To keep the whole Studio install on another drive,
39
+ set `TAPI_STUDIO_HOME` before launching:
40
+
41
+ ```powershell
42
+ $env:TAPI_STUDIO_HOME="F:\Tapi\Studio"
43
+ tapi studio
44
+ ```
45
+
46
+ For separate paths, use `--cache-dir` for downloaded zip artifacts and
47
+ `--install-dir` for extracted portable Studio server releases.
48
+
49
+ After a successful install, the CLI prunes older Tapi-managed download artifacts
50
+ from the cache and older SDK-marked release folders from the install directory.
51
+ The current artifact and current release are kept.
52
+
53
+ Useful commands:
54
+
55
+ ```bash
56
+ tapi init --project brokerage
57
+ tapi link --project brokerage
58
+ tapi studio
59
+ tapi service install --channel pilot
60
+ tapi service status
61
+ tapi apis generate
62
+ tapi sessions
63
+ tapi publish
64
+ ```
65
+
66
+ Local authoring files live in the app repo:
67
+
68
+ ```text
69
+ .tapi/project.json
70
+ .tapi/sitemaps/<site>.json
71
+ .tapi/apis/<site>/<api>.json
72
+ .tapi/generated/catalog.json
73
+ src/tapi.generated.ts
74
+ ```
75
+
76
+ Drafts are local. `tapi publish` uploads local sitemaps and API contracts, marks
77
+ ready requests as published, and activates one immutable server release. Runtime
78
+ SDK calls read the active release catalog, not mutable Studio drafts.
79
+
80
+ ```bash
81
+ TAPI_API_KEY=tapi_project_key tapi publish
82
+ TAPI_API_KEY=tapi_project_key tapi apis generate
83
+ ```
84
+
85
+ ## Quick Start
86
+
87
+ Create one TAPI client in server-side app code. Use a project-scoped API key and
88
+ the same project id from `.tapi/project.json`.
89
+
90
+ ```ts
91
+ import { TapiClient } from "@tapi-dev/sdk";
92
+
93
+ export const tapi = new TapiClient({
94
+ baseUrl: process.env.TAPI_BASE_URL!,
95
+ apiKey: process.env.TAPI_API_KEY!,
96
+ projectId: process.env.TAPI_PROJECT_ID!,
97
+ });
98
+ ```
99
+
100
+ Production is the default mode. Unknown website states fail the run instead of
101
+ holding a browser open:
102
+
103
+ ```ts
104
+ const tapi = new TapiClient({
105
+ baseUrl: process.env.TAPI_BASE_URL!,
106
+ apiKey: process.env.TAPI_API_KEY!,
107
+ projectId: process.env.TAPI_PROJECT_ID!,
108
+ dev: false,
109
+ });
110
+ ```
111
+
112
+ During development, set `dev: true`. If a website reaches an unknown state,
113
+ Tapi preserves the browser session for takeover instead of turning it into a
114
+ production failure:
115
+
116
+ ```ts
117
+ const tapi = new TapiClient({
118
+ baseUrl: process.env.TAPI_BASE_URL!,
119
+ apiKey: process.env.TAPI_API_KEY!,
120
+ projectId: process.env.TAPI_PROJECT_ID!,
121
+ dev: true,
122
+ });
123
+ ```
124
+
125
+ Inspect preserved sessions from the repo:
126
+
127
+ ```bash
128
+ tapi sessions
129
+ tapi sessions --json
130
+ tapi sessions open <session-id>
62
131
  ```
63
132
 
64
- ## Quick Start
65
-
66
- Create one TAPI client in server-side app code. Use a project-scoped API key and
67
- the same project id from `.tapi/project.json`.
68
-
69
- ```ts
70
- import { TapiClient } from "@tapi-dev/sdk";
71
-
72
- export const tapi = new TapiClient({
73
- baseUrl: process.env.TAPI_BASE_URL!,
74
- apiKey: process.env.TAPI_API_KEY!,
75
- projectId: process.env.TAPI_PROJECT_ID!,
76
- });
77
- ```
133
+ `tapi sessions open` reuses the matching Studio window for the current repo when
134
+ one is already running; otherwise it installs/starts Studio and opens directly
135
+ to that session.
78
136
 
79
137
  Generated website APIs are authored visually in Tapi Studio by setting workflow
80
138
  bounds, selecting inputs/outputs, and publishing an API. The SDK sees the public
81
139
  operation name:
82
-
83
- ```text
84
- <namespace>.<operation>
85
- ```
86
-
87
- ```ts
88
- const operation = await tapi.websiteApis.describe("schwab.place_order");
89
-
90
- const run = await tapi.websiteApis.run("schwab.place_order", {
91
- inputs: {
92
- symbol: "AAPL",
93
- side: "buy",
94
- quantity: 10,
95
- orderType: "limit",
96
- limitPrice: 190,
97
- },
98
- });
99
-
100
- const completedRun = await tapi.runs.wait(run.id);
101
- console.log(completedRun.status, completedRun.result);
102
- ```
103
-
104
- ## API-Call Triggers
105
-
106
- Triggers are scheduled calls to published website APIs. Keep trigger definitions
107
- in the app repo and sync them after publishing the API catalog:
108
-
109
- ```ts
110
- // tapi.config.ts
111
- export default {
112
- triggers: {
113
- nightlyBalance: {
114
- apiRequest: "schwab.get_balance",
115
- schedule: { cron: "0 9 * * MON-FRI" },
116
- inputs: { accountId: "main" },
117
- runtime: { profileRef: "perm_default" },
118
- },
119
- },
120
- };
121
- ```
122
-
123
- ```bash
124
- TAPI_API_KEY=tapi_project_key npx tapi triggers sync
125
- ```
126
-
127
- `triggers sync` upserts by trigger name for the current `.tapi/project.json`
128
- project. Schedules support standard five-field cron strings or intervals as
129
- numbers of seconds / strings like `30s`, `15m`, `2h`, and `1d`.
130
-
131
- You can also manage triggers directly:
132
-
133
- ```ts
134
- await tapi.triggers.create({
135
- name: "nightlyBalance",
136
- apiRequest: "schwab.get_balance",
137
- schedule: { cron: "0 9 * * MON-FRI" },
138
- inputs: { accountId: "main" },
139
- runtime: { profileRef: "perm_default" },
140
- });
141
-
142
- await tapi.triggers.fire("act_123");
143
- await tapi.triggers.disable("act_123");
144
- ```
145
-
146
- You can inspect the same input/output contract from the CLI:
147
-
148
- ```bash
149
- npx tapi apis describe schwab.place_order \
150
- --api-base-url "$TAPI_BASE_URL" \
151
- --api-key "$TAPI_API_KEY" \
152
- --project "$TAPI_PROJECT_ID"
153
- ```
154
-
155
- Do not expose `TAPI_API_KEY` in public browser bundles. Put the SDK behind your
156
- own backend route, server action, or job worker when using secret API keys.
140
+
141
+ ```text
142
+ <namespace>.<operation>
143
+ ```
144
+
145
+ ```ts
146
+ const operation = await tapi.websiteApis.describe("schwab.place_order");
147
+
148
+ const run = await tapi.websiteApis.run("schwab.place_order", {
149
+ inputs: {
150
+ symbol: "AAPL",
151
+ side: "buy",
152
+ quantity: 10,
153
+ orderType: "limit",
154
+ limitPrice: 190,
155
+ },
156
+ });
157
+
158
+ const completedRun = await tapi.runs.wait(run.id);
159
+ console.log(completedRun.status, completedRun.result);
160
+ ```
161
+
162
+ ## Late Inputs
163
+
164
+ Most API inputs are known before the run starts. Use `lateInput()` when the
165
+ workflow needs a value later, after earlier browser steps have triggered work
166
+ somewhere else.
167
+
168
+ For example, state 4 might submit a login form and trigger a one-time code by
169
+ email or SMS. State 5 needs that code, but your app can only retrieve it after
170
+ state 4 runs. Declare the input up front with the API call, then resolve it
171
+ from your own code:
172
+
173
+ ```ts
174
+ import { lateInput } from "@tapi-dev/sdk";
175
+
176
+ const verificationCode = lateInput<string>({
177
+ resolve: async () => {
178
+ return await waitForVerificationCodeFromEmail();
179
+ },
180
+ timeoutMs: 180_000,
181
+ });
182
+
183
+ const run = await tapi.websiteApis.run("walmart.login", {
184
+ inputs: {
185
+ email: "buyer@example.com",
186
+ password: process.env.WALMART_PASSWORD!,
187
+ verificationCode,
188
+ },
189
+ });
190
+
191
+ await verificationCode.waitForProvides();
192
+
193
+ const completedRun = await tapi.runs.wait(run.id);
194
+ ```
195
+
196
+ You can also provide the value manually. This is useful when another callback,
197
+ worker, webhook, or polling loop finds the value:
198
+
199
+ ```ts
200
+ const verificationCode = lateInput<string>({ timeoutMs: 180_000 });
201
+
202
+ const run = await tapi.websiteApis.run("walmart.login", {
203
+ inputs: { email: "buyer@example.com", verificationCode },
204
+ });
205
+
206
+ const code = await waitForVerificationCodeFromWebhook();
207
+ await verificationCode.set(code);
208
+
209
+ const completedRun = await tapi.runs.wait(run.id);
210
+ ```
211
+
212
+ Tapi only waits when the runner reaches the action bound to that input. If your
213
+ app resolves the value early, Tapi stores it and uses it later. If the value is
214
+ not provided before `timeoutMs`, the run fails instead of guessing or using the
215
+ recorded default.
216
+
217
+ Late inputs are for values produced outside the website flow. Values read from
218
+ the website itself should stay as normal API outputs.
219
+
220
+ ## API-Call Triggers
221
+
222
+ Triggers are scheduled calls to published website APIs. Keep trigger definitions
223
+ in the app repo and sync them after publishing the API catalog:
224
+
225
+ ```ts
226
+ // tapi.config.ts
227
+ export default {
228
+ triggers: {
229
+ nightlyBalance: {
230
+ apiRequest: "schwab.get_balance",
231
+ schedule: { cron: "0 9 * * MON-FRI" },
232
+ inputs: { accountId: "main" },
233
+ runtime: { profileRef: "perm_default" },
234
+ },
235
+ },
236
+ };
237
+ ```
238
+
239
+ ```bash
240
+ TAPI_API_KEY=tapi_project_key tapi triggers sync
241
+ ```
242
+
243
+ `triggers sync` upserts by trigger name for the current `.tapi/project.json`
244
+ project. Schedules support standard five-field cron strings or intervals as
245
+ numbers of seconds / strings like `30s`, `15m`, `2h`, and `1d`.
246
+
247
+ You can also manage triggers directly:
248
+
249
+ ```ts
250
+ await tapi.triggers.create({
251
+ name: "nightlyBalance",
252
+ apiRequest: "schwab.get_balance",
253
+ schedule: { cron: "0 9 * * MON-FRI" },
254
+ inputs: { accountId: "main" },
255
+ runtime: { profileRef: "perm_default" },
256
+ });
257
+
258
+ await tapi.triggers.fire("act_123");
259
+ await tapi.triggers.disable("act_123");
260
+ ```
261
+
262
+ You can inspect the same input/output contract from the CLI:
263
+
264
+ ```bash
265
+ tapi apis describe schwab.place_order \
266
+ --api-base-url "$TAPI_BASE_URL" \
267
+ --api-key "$TAPI_API_KEY" \
268
+ --project "$TAPI_PROJECT_ID"
269
+ ```
270
+
271
+ Do not expose `TAPI_API_KEY` in public browser bundles. Put the SDK behind your
272
+ own backend route, server action, or job worker when using secret API keys.
157
273
 
158
274
  ## Runtime Profiles and Proxies
159
275
 
@@ -224,17 +340,17 @@ const tapi = new TapiClient({
224
340
  });
225
341
  ```
226
342
 
227
- ## Configuration
228
-
229
- ```env
230
- TAPI_BASE_URL=https://your-tapi-api-host
231
- TAPI_API_KEY=tapi_project_key
232
- TAPI_PROJECT_ID=brokerage
233
- ```
234
-
235
- `projectId` is optional only when the API key itself is project-scoped. If
236
- provided, the SDK sends it as the `X-Tapi-Project` header and the server rejects
237
- mismatches.
343
+ ## Configuration
344
+
345
+ ```env
346
+ TAPI_BASE_URL=https://your-tapi-api-host
347
+ TAPI_API_KEY=tapi_project_key
348
+ TAPI_PROJECT_ID=brokerage
349
+ ```
350
+
351
+ `projectId` is optional only when the API key itself is project-scoped. If
352
+ provided, the SDK sends it as the `X-Tapi-Project` header and the server rejects
353
+ mismatches.
238
354
 
239
355
  ## Common Project Setup
240
356
 
@@ -250,12 +366,12 @@ src/
250
366
  // src/lib/tapi.ts
251
367
  import { TapiClient } from "@tapi-dev/sdk";
252
368
 
253
- export const tapi = new TapiClient({
254
- baseUrl: process.env.TAPI_BASE_URL!,
255
- apiKey: process.env.TAPI_API_KEY!,
256
- projectId: process.env.TAPI_PROJECT_ID!,
257
- });
258
- ```
369
+ export const tapi = new TapiClient({
370
+ baseUrl: process.env.TAPI_BASE_URL!,
371
+ apiKey: process.env.TAPI_API_KEY!,
372
+ projectId: process.env.TAPI_PROJECT_ID!,
373
+ });
374
+ ```
259
375
 
260
376
  Application code should import this shared client instead of constructing a new client in every file.
261
377
 
@@ -280,126 +396,127 @@ const run = await tapi.websiteApis.run("apiName.requestKey", {
280
396
  });
281
397
 
282
398
  await tapi.runs.get(run.id);
283
- await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
284
- await tapi.runs.cancel(run.id);
285
- ```
286
-
287
- ## Cloud Batch Runs
288
-
289
- Cloud runs are requested from the SDK, but VM provisioning, service
290
- installation, worker leases, browser internals, and AWS cleanup stay on the
291
- Tapi server. The SDK is only the front door.
292
-
293
- Keep this code server-side. Do not put `TAPI_API_KEY` in a browser bundle and
294
- do not wire AWS or Stripe from the developer's app. The app sends its own user
295
- identity to Tapi, and Tapi handles prepaid Stripe checkout, credit accounting,
296
- AWS worker provisioning, and cleanup.
297
-
298
- The minimum developer flow is:
299
-
300
- 1. App user clicks a button.
301
- 2. The developer's backend calls `runCloudBatch` with `user.externalUserId`.
302
- 3. If the returned run has `status: "payment_required"`, redirect the app user
303
- to `run.checkoutUrl`.
304
- 4. Stripe calls the Tapi webhook, Tapi credits that same `externalUserId`, and
305
- the app retries the cloud batch.
306
- 5. If credit is available, Tapi reserves the balance and starts cloud workers.
307
-
308
- ```ts
309
- // backend route or server action
310
- const quote = await tapi.websiteApis.quoteCloud("schwab.place_order", {
311
- inputs: [
312
- { symbol: "AAPL", quantity: 1 },
313
- { symbol: "MSFT", quantity: 2 },
314
- ],
315
- user: {
316
- externalUserId: appUser.id,
317
- email: appUser.email,
318
- },
319
- cloud: {
320
- windows: true,
321
- maxVms: 1,
322
- chromePerVm: 10,
323
- maxCostUsd: 20,
324
- },
325
- });
326
-
327
- if (!quote.withinMaxCost) {
328
- throw new Error("Cloud batch exceeds the configured cost cap");
329
- }
330
-
331
- const run = await tapi.websiteApis.runCloudBatch("schwab.place_order", {
332
- inputs: [
333
- { symbol: "AAPL", quantity: 1 },
334
- { symbol: "MSFT", quantity: 2 },
335
- ],
336
- user: {
337
- externalUserId: appUser.id,
338
- email: appUser.email,
339
- },
340
- payment: {
341
- successUrl: `https://your-app.example/cloud/success`,
342
- cancelUrl: `https://your-app.example/cloud/cancel`,
343
- },
344
- cloud: {
345
- windows: true,
346
- maxVms: 1,
347
- chromePerVm: 10,
348
- maxCostUsd: 20,
349
- },
350
- });
351
-
352
- if (run.status === "payment_required") {
353
- return { redirectTo: run.checkoutUrl };
354
- }
355
-
356
- for await (const event of tapi.cloudRuns.stream(run.id)) {
357
- console.log(event.status, event.traceId);
358
- }
359
-
360
- const results = await tapi.cloudRuns.results(run.id);
361
- ```
362
-
363
- When `CLOUD_BILLING_ENABLED=true`, the server refuses to launch AWS workers
364
- unless prepaid cloud credit is available and reserved first. If balance is too
365
- low, `runCloudBatch` converts the server's HTTP `402` into a normal
366
- `CloudBatchRun` object with `status: "payment_required"`, `checkoutUrl`,
367
- `availableBalanceCents`, and `requiredBalanceCents`. That keeps the button
368
- handler simple.
369
-
370
- Credits are scoped under the Tapi API key owner, app id, and
371
- `user.externalUserId`. Use the same `externalUserId` for quote, balance,
372
- checkout, and run calls. If you omit `externalUserId` but provide `email`, Tapi
373
- uses the normalized email as the external user id.
374
-
375
- Applications can also create a top-up checkout explicitly:
376
-
377
- ```ts
378
- const user = {
379
- externalUserId: appUser.id,
380
- email: appUser.email,
381
- };
382
-
383
- const balance = await tapi.cloudRuns.balance(user);
384
-
385
- if (balance.balanceCents < balance.minimumBalanceCents) {
386
- const checkout = await tapi.cloudRuns.checkout({
387
- amountCents: balance.minimumBalanceCents,
388
- user,
389
- successUrl: "https://your-app.example/cloud/success",
390
- cancelUrl: "https://your-app.example/cloud/cancel",
391
- });
392
- console.log(checkout.checkoutUrl);
393
- }
394
- ```
395
-
396
- The developer application does not need VM provisioning logic, installer
397
- deployment logic, worker command interpretation, browser runner internals, or
398
- builder logic. Those stay behind the Tapi server API.
399
-
400
- Website API requests are addressed as `<namespace>.<operation>`. The namespace
401
- comes from the generated website name in Studio. The operation is the name the
402
- developer types into the SDK name textbox at the workflow boundary.
399
+ await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
400
+ await tapi.runs.provideInput(run.id, "verificationCode", "123456");
401
+ await tapi.runs.cancel(run.id);
402
+ ```
403
+
404
+ ## Cloud Batch Runs
405
+
406
+ Cloud runs are requested from the SDK, but VM provisioning, service
407
+ installation, worker leases, browser internals, and AWS cleanup stay on the
408
+ Tapi server. The SDK is only the front door.
409
+
410
+ Keep this code server-side. Do not put `TAPI_API_KEY` in a browser bundle and
411
+ do not wire AWS or Stripe from the developer's app. The app sends its own user
412
+ identity to Tapi, and Tapi handles prepaid Stripe checkout, credit accounting,
413
+ AWS worker provisioning, and cleanup.
414
+
415
+ The minimum developer flow is:
416
+
417
+ 1. App user clicks a button.
418
+ 2. The developer's backend calls `runCloudBatch` with `user.externalUserId`.
419
+ 3. If the returned run has `status: "payment_required"`, redirect the app user
420
+ to `run.checkoutUrl`.
421
+ 4. Stripe calls the Tapi webhook, Tapi credits that same `externalUserId`, and
422
+ the app retries the cloud batch.
423
+ 5. If credit is available, Tapi reserves the balance and starts cloud workers.
424
+
425
+ ```ts
426
+ // backend route or server action
427
+ const quote = await tapi.websiteApis.quoteCloud("schwab.place_order", {
428
+ inputs: [
429
+ { symbol: "AAPL", quantity: 1 },
430
+ { symbol: "MSFT", quantity: 2 },
431
+ ],
432
+ user: {
433
+ externalUserId: appUser.id,
434
+ email: appUser.email,
435
+ },
436
+ cloud: {
437
+ windows: true,
438
+ maxVms: 1,
439
+ chromePerVm: 10,
440
+ maxCostUsd: 20,
441
+ },
442
+ });
443
+
444
+ if (!quote.withinMaxCost) {
445
+ throw new Error("Cloud batch exceeds the configured cost cap");
446
+ }
447
+
448
+ const run = await tapi.websiteApis.runCloudBatch("schwab.place_order", {
449
+ inputs: [
450
+ { symbol: "AAPL", quantity: 1 },
451
+ { symbol: "MSFT", quantity: 2 },
452
+ ],
453
+ user: {
454
+ externalUserId: appUser.id,
455
+ email: appUser.email,
456
+ },
457
+ payment: {
458
+ successUrl: `https://your-app.example/cloud/success`,
459
+ cancelUrl: `https://your-app.example/cloud/cancel`,
460
+ },
461
+ cloud: {
462
+ windows: true,
463
+ maxVms: 1,
464
+ chromePerVm: 10,
465
+ maxCostUsd: 20,
466
+ },
467
+ });
468
+
469
+ if (run.status === "payment_required") {
470
+ return { redirectTo: run.checkoutUrl };
471
+ }
472
+
473
+ for await (const event of tapi.cloudRuns.stream(run.id)) {
474
+ console.log(event.status, event.traceId);
475
+ }
476
+
477
+ const results = await tapi.cloudRuns.results(run.id);
478
+ ```
479
+
480
+ When `CLOUD_BILLING_ENABLED=true`, the server refuses to launch AWS workers
481
+ unless prepaid cloud credit is available and reserved first. If balance is too
482
+ low, `runCloudBatch` converts the server's HTTP `402` into a normal
483
+ `CloudBatchRun` object with `status: "payment_required"`, `checkoutUrl`,
484
+ `availableBalanceCents`, and `requiredBalanceCents`. That keeps the button
485
+ handler simple.
486
+
487
+ Credits are scoped under the Tapi API key owner, app id, and
488
+ `user.externalUserId`. Use the same `externalUserId` for quote, balance,
489
+ checkout, and run calls. If you omit `externalUserId` but provide `email`, Tapi
490
+ uses the normalized email as the external user id.
491
+
492
+ Applications can also create a top-up checkout explicitly:
493
+
494
+ ```ts
495
+ const user = {
496
+ externalUserId: appUser.id,
497
+ email: appUser.email,
498
+ };
499
+
500
+ const balance = await tapi.cloudRuns.balance(user);
501
+
502
+ if (balance.balanceCents < balance.minimumBalanceCents) {
503
+ const checkout = await tapi.cloudRuns.checkout({
504
+ amountCents: balance.minimumBalanceCents,
505
+ user,
506
+ successUrl: "https://your-app.example/cloud/success",
507
+ cancelUrl: "https://your-app.example/cloud/cancel",
508
+ });
509
+ console.log(checkout.checkoutUrl);
510
+ }
511
+ ```
512
+
513
+ The developer application does not need VM provisioning logic, installer
514
+ deployment logic, worker command interpretation, browser runner internals, or
515
+ builder logic. Those stay behind the Tapi server API.
516
+
517
+ Website API requests are addressed as `<namespace>.<operation>`. The namespace
518
+ comes from the generated website name in Studio. The operation is the name the
519
+ developer types into the SDK name textbox at the workflow boundary.
403
520
 
404
521
  `describe()` returns the public SDK contract: input controls such as
405
522
  `textbox`, `radio`, `select`, `checkbox`, conditional requirements such as
@@ -429,6 +546,7 @@ The package includes generated TypeScript declarations. Common exported types in
429
546
 
430
547
  ```ts
431
548
  import type {
549
+ LateInput,
432
550
  RuntimeRequirements,
433
551
  RuntimeProfile,
434
552
  RuntimeRunOptions,