@tapi-dev/sdk
v0.1.40
Published
Official JavaScript and TypeScript client for TAPI developer APIs.
Readme
TAPI JavaScript SDK
Official JavaScript and TypeScript client for TAPI developer APIs.
Install
npm install @tapi-dev/sdkThis package is ESM-first and works in runtimes with fetch, including modern Node.js and browser-like server runtimes.
The CLI command is tapi. If your package manager only installed the SDK
locally and tapi is not on PATH, use npx tapi as a fallback.
Developer Flow
Tapi Studio is launched from the signed-in developer account. Running
tapi studio lists the Tapps available to that account, asks which one to open,
then starts Studio in that Tapp context. The current directory can still provide
a default via .tapi/project.json, but it is no longer required for Studio to
know which Tapp to show.
npm install @tapi-dev/sdk
tapi login
tapi tapp create brokerage
tapi studioUseful Studio launch forms:
tapi studio # ask which Tapp to open
tapi studio --tapp brokerage # open one Tapp directly
tapi studio --select # force the picker
tapi studio --last # reuse the last selected Tapptapi studio checks the required local tapi-service, installs it when needed,
downloads the portable Studio server release when the channel manifest uses
installerKind: portable-server, starts Studio locally, and opens the browser.
If the channel still publishes a legacy NSIS desktop Studio manifest and no
Studio executable is installed yet, tapi studio runs the installer first, then
opens Studio. The normal developer path is the portable server launched by the
CLI.
By default, Studio downloads and extracted portable server releases live under
the user's local app data directory. On Windows that is usually
%LOCALAPPDATA%\Tapi\Studio. To keep the whole Studio install on another drive,
set TAPI_STUDIO_HOME before launching:
$env:TAPI_STUDIO_HOME="F:\Tapi\Studio"
tapi studioFor separate paths, use --cache-dir for downloaded zip artifacts and
--install-dir for extracted portable Studio server releases.
After a successful install, the CLI prunes older Tapi-managed download artifacts from the cache and older SDK-marked release folders from the install directory. The current artifact and current release are kept.
Useful commands:
tapi login
tapi init --project brokerage
tapi link --project brokerage
tapi studio
tapi studio --tapp brokerage
tapi service install --channel pilot
tapi service status
tapi services describe schwab.place_order
tapi tapp create brokerage
tapi queue create "John"
tapi tapp queue add brokerage queue_abc123
tapi runner setup --runner-id john-laptop --project brokerage
tapi runner slot set john-laptop 1 queue_abc123 --project brokerage
tapi tapp service add brokerage schwab.place_order --service-map sm_123 --entry place_order
tapi triggers sync
tapi sessionsLocal project identity lives in the app repo:
.tapi/project.json
.tapi/services/catalog.jsonServiceMaps and service-run contracts are saved in the bound Tapi server project. Runtime SDK calls read the service catalog from that project.
tapi tapp create brokerage --name "Brokerage"
tapi tapp service add brokerage schwab.place_order \
--service-map sm_123 \
--entry place_orderCreate a queue, attach it to the Tapp, and point one or more runner slots at that queue:
tapi queue create "John" --project brokerage
tapi tapp queue add brokerage queue_abc123
tapi runner slot set john-laptop 1 queue_abc123 \
--project brokerageFor users setting up a local runner, tapi runner setup opens a localhost setup
page that can create or accept a queue id, attach that queue to the Tapp, and
assign one or more slots on the runner:
tapi runner setup \
--runner-id john-laptop \
--project brokerageQuick Start
Create one TAPI client in server-side app code. Use a Firebase ID token for the
developer identity that owns and pays for the Tapp, the Tapp id that owns the
service call, and optionally the same project id from .tapi/project.json.
import { TapiClient } from "@tapi-dev/sdk";
export const tapi = new TapiClient({
baseUrl: process.env.TAPI_BASE_URL!,
authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
tappId: process.env.TAPI_TAPP_ID!,
projectId: process.env.TAPI_PROJECT_ID!,
});Production is the default mode. Unknown website states fail the run instead of holding a browser open:
const tapi = new TapiClient({
baseUrl: process.env.TAPI_BASE_URL!,
authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
tappId: process.env.TAPI_TAPP_ID!,
projectId: process.env.TAPI_PROJECT_ID!,
dev: false,
});During development, set dev: true. If a website reaches an unknown state,
Tapi preserves the browser session for takeover instead of turning it into a
production failure:
const tapi = new TapiClient({
baseUrl: process.env.TAPI_BASE_URL!,
authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
tappId: process.env.TAPI_TAPP_ID!,
projectId: process.env.TAPI_PROJECT_ID!,
dev: true,
});Inspect preserved sessions from the repo:
tapi sessions
tapi sessions --json
tapi sessions open <session-id>tapi sessions open reuses the matching Studio window for the current repo when
one is already running; otherwise it installs/starts Studio and opens directly
to that session.
Service runs are authored visually in Tapi Studio by setting ServiceMap bounds and selecting inputs/outputs. The SDK sees the public service-run name:
<namespace>.<operation>A service run is routed through a queue. The queue id is the only routing handle: the Tapp must attach the queue, and one or more runner slots must be configured with the same queue id.
const operation = await tapi.services.describe("schwab.place_order");
const run = await tapi.services.run("schwab.place_order", {
queueId: "queue_abc123",
inputs: {
symbol: "AAPL",
side: "buy",
quantity: 10,
orderType: "limit",
limitPrice: 190,
},
});
const completedRun = await tapi.runs.wait(run.id);
console.log(completedRun.status, completedRun.result);Late Inputs
Most service inputs are known before the run starts. Use lateInput() when the
workflow needs a value later, after earlier browser steps have triggered work
somewhere else.
For example, state 4 might submit a login form and trigger a one-time code by email or SMS. State 5 needs that code, but your app can only retrieve it after state 4 runs. Declare the input up front with the service run, then resolve it from your own code:
import { lateInput } from "@tapi-dev/sdk";
const verificationCode = lateInput<string>({
resolve: async () => {
return await waitForVerificationCodeFromEmail();
},
timeoutMs: 180_000,
});
const run = await tapi.services.run("walmart.login", {
inputs: {
email: "[email protected]",
password: process.env.WALMART_PASSWORD!,
verificationCode,
},
});
await verificationCode.waitForProvides();
const completedRun = await tapi.runs.wait(run.id);You can also provide the value manually. This is useful when another callback, worker, webhook, or polling loop finds the value:
const verificationCode = lateInput<string>({ timeoutMs: 180_000 });
const run = await tapi.services.run("walmart.login", {
inputs: { email: "[email protected]", verificationCode },
});
const code = await waitForVerificationCodeFromWebhook();
await verificationCode.set(code);
const completedRun = await tapi.runs.wait(run.id);Tapi only waits when the runner reaches the action bound to that input. If your
app resolves the value early, Tapi stores it and uses it later. If the value is
not provided before timeoutMs, the run fails instead of guessing or using the
recorded default.
Late inputs are for values produced outside the website flow. Values read from the website itself should stay as normal service outputs.
Service Triggers
Triggers are scheduled service runs. Keep trigger definitions in the app repo and sync them with the service catalog from that repo:
// tapi.config.ts
export default {
triggers: {
nightlyBalance: {
serviceRun: "schwab.get_balance",
schedule: { cron: "0 9 * * MON-FRI" },
inputs: { accountId: "main" },
runtime: { profileRef: "perm_default" },
},
},
};tapi triggers synctriggers sync upserts by trigger name for the current .tapi/project.json
project. Schedules support standard five-field cron strings or intervals as
numbers of seconds / strings like 30s, 15m, 2h, and 1d.
You can also manage triggers directly:
await tapi.triggers.create({
name: "nightlyBalance",
serviceRun: "schwab.get_balance",
schedule: { cron: "0 9 * * MON-FRI" },
inputs: { accountId: "main" },
runtime: { profileRef: "perm_default" },
});
await tapi.triggers.fire("act_123");
await tapi.triggers.disable("act_123");You can inspect the same input/output contract from the CLI:
tapi services describe schwab.place_order \
--api-base-url "$TAPI_BASE_URL" \
--project "$TAPI_PROJECT_ID"Do not expose developer Firebase ID tokens in public browser bundles. Put the SDK behind your own backend route, server action, or job worker.
Runtime Profiles and Proxies
Browser identity is selected with a runner-local profileRef, not by passing
Chrome folder paths through the cloud service-run request.
Use PermProfile for a long-lived account identity. It owns a persistent
Chrome user-data directory and should usually use the runner's normal home IP:
const perm = await tapi.runtime.permProfiles.create({
displayName: "Mom Walmart",
originPolicy: { type: "home" },
});
await tapi.runtime.permProfiles.launchSetup(perm.profile.profileRef);
// The user logs in, adds passkeys, then closes setup Chrome.
await tapi.services.run("walmart.reorder", {
queueId: "queue_abc123",
inputs: { item: "paper towels" },
runtime: { profileRef: perm.profile.profileRef },
});Use TempProfile for throwaway work. Temp profiles can be cloned from a
template profile and assigned different proxies:
const temp = await tapi.runtime.tempProfiles.provisionMany({
count: 3,
displayNamePrefix: "checkout",
proxies: [
"http://user1:[email protected]:8080",
"http://user2:[email protected]:8080",
"http://user3:[email protected]:8080",
],
destroyOnRelease: true,
});
await Promise.all(
temp.profiles.map((profile) =>
tapi.services.run("walmart.lookupItem", {
queueId: "queue_abc123",
inputs: { sku: "123" },
runtime: {
profileRef: profile.profileRef,
destroyOnRelease: profile.destroyOnRelease,
},
}),
),
);runtime.profileRef is the only value sent to the TAPI cloud run request. Proxy
URLs, proxy credentials, and Chrome profile folders stay on the local runner
and are handled through the local control WebSocket at ws://127.0.0.1:8765.
When the same PermProfile is used concurrently for multiple different sites,
the runner creates runtime clones so Chrome does not open the same
--user-data-dir twice. You can also create a clone explicitly with
tapi.runtime.profiles.clone({ profileRef, site }), but most apps should pass
the base profileRef and let the runner decide.
If your JavaScript runtime does not provide WebSocket, pass one:
const tapi = new TapiClient({
baseUrl: process.env.TAPI_BASE_URL!,
authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
tappId: process.env.TAPI_TAPP_ID!,
webSocket: MyWebSocketImplementation,
});Configuration
TAPI_BASE_URL=https://your-tapi-api-host
TAPI_FIREBASE_ID_TOKEN=firebase_id_token_for_the_tapp_owner
TAPI_TAPP_ID=brokerage
TAPI_PROJECT_ID=brokeragetappId is required. Service calls are made for that Tapp, and the developer
who owns the Tapp pays for the service delivery. If provided, projectId is
sent as the X-Tapi-Project header and the server rejects mismatches.
Common Project Setup
A typical application keeps the client in one small module:
src/
lib/
tapi.ts// src/lib/tapi.ts
import { TapiClient } from "@tapi-dev/sdk";
export const tapi = new TapiClient({
baseUrl: process.env.TAPI_BASE_URL!,
authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
tappId: process.env.TAPI_TAPP_ID!,
projectId: process.env.TAPI_PROJECT_ID!,
});Application code should import this shared client instead of constructing a new client in every file.
Available Resources
await tapi.catalog.get();
await tapi.runners.list();
await tapi.runtime.requirements();
await tapi.runtime.profiles.list();
await tapi.runtime.profiles.clone({ profileRef: "perm_default", site: "walmart" });
await tapi.runtime.permProfiles.create({ displayName: "Default", originPolicy: { type: "home" } });
await tapi.runtime.tempProfiles.provisionMany({ proxies: ["http://user:pass@host:8080"] });
await tapi.services.describe("schwab.place_order");
const run = await tapi.services.run("serviceName.serviceKey", {
queueId: "queue_abc123",
inputs: { example: true },
runtime: { profileRef: "perm_default" },
priority: 5,
idempotencyKey: "request-123",
});
await tapi.runs.get(run.id);
await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
await tapi.runs.provideInput(run.id, "verificationCode", "123456");
await tapi.runs.cancel(run.id);Cloud Batch Runs
Cloud runs are requested from the SDK, but VM provisioning, service installation, worker leases, browser internals, and AWS cleanup stay on the Tapi server. The SDK is only the front door.
Keep this code server-side. Do not put developer Firebase ID tokens in a browser bundle and do not wire AWS or Stripe from the developer's app. The app sends its own user identity to Tapi, and Tapi handles prepaid Stripe checkout, credit accounting, AWS worker provisioning, and cleanup.
The minimum developer flow is:
- App user clicks a button.
- The developer's backend calls
runCloudBatchwithuser.externalUserId. - If the returned run has
status: "payment_required", redirect the app user torun.checkoutUrl. - Stripe calls the Tapi webhook, Tapi credits that same
externalUserId, and the app retries the cloud batch. - If credit is available, Tapi reserves the balance and starts cloud workers.
// backend route or server action
const quote = await tapi.services.quoteCloud("schwab.place_order", {
inputs: [
{ symbol: "AAPL", quantity: 1 },
{ symbol: "MSFT", quantity: 2 },
],
user: {
externalUserId: appUser.id,
email: appUser.email,
},
cloud: {
windows: true,
maxVms: 1,
chromePerVm: 10,
maxCostUsd: 20,
},
});
if (!quote.withinMaxCost) {
throw new Error("Cloud batch exceeds the configured cost cap");
}
const run = await tapi.services.runCloudBatch("schwab.place_order", {
inputs: [
{ symbol: "AAPL", quantity: 1 },
{ symbol: "MSFT", quantity: 2 },
],
user: {
externalUserId: appUser.id,
email: appUser.email,
},
payment: {
successUrl: `https://your-app.example/cloud/success`,
cancelUrl: `https://your-app.example/cloud/cancel`,
},
cloud: {
windows: true,
maxVms: 1,
chromePerVm: 10,
maxCostUsd: 20,
},
});
if (run.status === "payment_required") {
return { redirectTo: run.checkoutUrl };
}
for await (const event of tapi.cloudRuns.stream(run.id)) {
console.log(event.status, event.traceId);
}
const results = await tapi.cloudRuns.results(run.id);When CLOUD_BILLING_ENABLED=true, the server refuses to launch AWS workers
unless prepaid cloud credit is available and reserved first. If balance is too
low, runCloudBatch converts the server's HTTP 402 into a normal
CloudBatchRun object with status: "payment_required", checkoutUrl,
availableBalanceCents, and requiredBalanceCents. That keeps the button
handler simple.
Credits are scoped under the authenticated Firebase owner, Tapp id, and
user.externalUserId. Use the same externalUserId for quote, balance,
checkout, and run calls. If you omit externalUserId but provide email, Tapi
uses the normalized email as the external user id.
Applications can also create a top-up checkout explicitly:
const user = {
externalUserId: appUser.id,
email: appUser.email,
};
const balance = await tapi.cloudRuns.balance(user);
if (balance.balanceCents < balance.minimumBalanceCents) {
const checkout = await tapi.cloudRuns.checkout({
amountCents: balance.minimumBalanceCents,
user,
successUrl: "https://your-app.example/cloud/success",
cancelUrl: "https://your-app.example/cloud/cancel",
});
console.log(checkout.checkoutUrl);
}The developer application does not need VM provisioning logic, installer deployment logic, worker command interpretation, browser runner internals, or builder logic. Those stay behind the Tapi server API.
Service runs are addressed as <serviceName>.<serviceKey>. The service name
comes from the ServiceMap name in Studio. The service key is the name the
developer types into the SDK name textbox at the service boundary.
describe() returns the public SDK contract: input controls such as
textbox, radio, select, checkbox, conditional requirements such as
requiredWhen, and the output schema. It does not expose workflow ids, state
ids, action ids, selectors, runner ids, or other Studio internals.
Errors
Failed HTTP responses throw TapiError:
import { TapiError } from "@tapi-dev/sdk";
try {
await tapi.catalog.get();
} catch (error) {
if (error instanceof TapiError) {
console.error(error.status, error.code, error.details);
}
throw error;
}TypeScript
The package includes generated TypeScript declarations. Common exported types include:
import type {
LateInput,
RuntimeRequirements,
RuntimeProfile,
RuntimeRunOptions,
SdkCatalog,
ServiceOperation,
ServiceRunRequest,
TapiRun,
TapiRunner,
} from "@tapi-dev/sdk";Local Development
From this SDK directory:
npm ci
npm test
npm run build
npm pack --dry-runnpm pack --dry-run shows the exact files that will be published.
