Skip to content

API clients

The API clients do from code what the CLI does from a terminal: deploy Workers and static sites, share apps, read the audit log, and manage bypass tokens, secrets and config values. Use them in scripts, scheduled jobs and internal tooling.

They are a different package from the SDK your app uses to check who is calling it. See SDK overview for that one.

Terminal window
npm install @jiayang-cloud/client

Node 20 or newer, Deno, Bun and Workers. No dependencies.

The clients use a CLI session. Run jiayang login once on the computer, and the client finds the session it saved:

import { Jiayang } from "@jiayang-cloud/client";
const jy = new Jiayang();
const me = await jy.me();
console.log(me.email, me.workspaces.map((w) => w.slug));

The client looks for a token in this order:

  1. The token you pass: new Jiayang({ token }) or Client(token).
  2. JIAYANG_TOKEN.
  3. The session jiayang login saved, in the same place the CLI keeps it. See Sign in.

A CLI session lasts 30 days. It can do what you can, except pay, change single sign-on or SCIM, delete or export a workspace, or open a path of an app to everyone. Those stay in the dashboard. jiayang sessions lists your sessions, and jiayang logout --everywhere ends them all.

In GitHub Actions, the deploy action’s credential works too, for deploying the one app it was made for and nothing else. An app’s bypass token doesn’t work: it calls the app, not the API. The client says so before sending it anywhere.

The API’s address is the one you pass (baseUrl in Node, base_url in Python), then JIAYANG_API, then https://api.jiayang.cloud. The session jiayang login saved goes only to the API it was made by, as the CLI’s does: if you pass an address or set JIAYANG_API to anywhere else, the client throws ConfigError instead of sending it. The client sends a token only over https, or plain http to this computer, and follows no redirect.

Behind a company proxy that inspects TLS, the Python client trusts the proxy’s certificate wherever your operating system does. Node trusts its own list, so point NODE_EXTRA_CA_CERTS at the proxy’s certificate, in PEM.

A static site goes up as its files. Only files the platform hasn’t got are uploaded, so a redeploy sends what changed.

import { readFile } from "node:fs/promises";
const version = await jy.deploy.site("acme", "handbook", {
files: [
{ path: "/index.html", content: "<h1>Handbook</h1>" },
{ path: "/logo.png", content: await readFile("logo.png") },
],
});
console.log(`version ${version.number} is live`);

A Worker goes up as the modules you built. A module ending in .js or .mjs is an ES module, .wasm is WebAssembly, and anything else is text or bytes, unless you give its type.

await jy.deploy.worker("acme", "hello", {
main_module: "index.js",
compatibility_date: "2026-09-01",
modules: { "index.js": { content: await readFile("dist/index.js", "utf8") } },
});

To give the app key-value storage or a queue, as "kv" and "queue" in jiayang.json do, add kv: true and queue: { path: "/jobs/queue" } to the bundle in Node.js, or pass kv=True and queue="/jobs/queue" in Python.

Both wait until the version is live and return it. If it doesn’t go live, they throw DeployFailedError with the reason the platform gave. They also take:

Node.js Python What it does
preview: true preview=True Puts it up at a preview address and leaves the live version as it is
wait: false wait=False Returns as soon as the version is on its way live
waitTimeoutMs wait_timeout How long to wait for it, and for another version of the app going live first. 30 minutes.
onProgress on_progress Called with a line each time something happens
scan scan= The project’s lock files and its .jiayang-allow, for the deploy’s scan

Every deploy is scanned for keys in its files. Its packages are checked for published vulnerabilities only if you send the lock files that pin them, whole, as scan. The CLI and the dashboard find them for you; the clients don’t look through your project. Without them the scan says no lock file, and a workspace that refuses deploys with vulnerable packages can’t refuse this one. .jiayang-allow goes the same way, as allow:

const scan = {
lockfiles: [{ path: "package-lock.json", text: await readFile("package-lock.json", "utf8") }],
allow: await readFile(".jiayang-allow", "utf8"),
};
await jy.deploy.site("acme", "handbook", { files }, { scan });

Up to 8 lock files, 8 MiB together, and 64 KiB of allow file.

The app has to exist first: apps.create. Container apps are built and pushed by jiayang deploy, which needs Docker. The clients don’t build anything or check your files the way the CLI does, so leave out anything you don’t want served. versions.rollback and versions.promote put an earlier version or a preview live.

await jy.access.share("acme", "hello", "ada@example.com", { role: "editor" });
await jy.access.shareDomain("acme", "hello", "acme.com");
await jy.access.shareGroup("acme", "hello", "finance");
const { still_shared_by } = await jy.access.unshare("acme", "hello", "ada@example.com");

Access starts and ends within 60 seconds, as it does from the CLI. unshare says what still lets the person in, if anything: their domain, a group, or the whole workspace. access.list returns everyone an app is shared with. See Share an app.

audit.iterate goes back through the whole log, newest first, a page at a time. Entries older than 30 days come from the archive, which can take a moment.

for await (const entry of jy.audit.iterate("acme", { app: "hello", decision: "deny" })) {
console.log(entry.at, entry.action, entry.reason);
}

audit.list reads one page, with limit (up to 500), before and before_id to page back, or after and after_id to follow new entries. Its answer has next when the platform stopped short of a full page and there is more behind it. Times are strings, as the API sends them, to the microsecond. See Audit log for who can read it and what each entry means.

const { token, secret, url } = await jy.tokens.create("acme", "hello", { name: "nightly", role: "viewer", expires_in_days: 30 });
// Send secret as `Authorization: Bearer <secret>` to url. It is shown once.
await jy.tokens.revoke("acme", "hello", token.id);

A revoked token stops working at once. See Tokens for scripts and machines.

Area Node.js Python
You me() me()
Workspaces workspaces.list, get, create the same
Members members.list, set, remove the same
Apps apps.list, get, create, update, delete the same
Versions versions.list, rollback, promote the same
Deploys deploy.worker, deploy.site the same
Sharing access.list, share, unshare, shareDomain, unshareDomain, shareGroup, unshareGroup share_domain, unshare_domain, share_group, unshare_group
Bypass tokens tokens.list, create, revoke the same
Audit log audit.list, iterate the same
Secrets secrets.list, set, delete the same
Config values env.list, update the same

Answers are the API’s own JSON, with its field names, such as active_version and created_at. The Node client has TypeScript types for each, and the Python client has TypedDicts.

Every error is a JiayangError. When the API refuses something, the error is an ApiError with status, code (the platform’s name for it, like not_found) and message, which you can show to people. Each status has a class of its own:

Status Class
400 BadRequestError
401 AuthenticationError: sign in again with jiayang login
403 PermissionDeniedError, or PlanLimitError with the limit the plan hit
404 NotFoundError: there’s nothing there, or nothing you may see
409 ConflictError
413 PayloadTooLargeError
429 RateLimitError, with retryAfter (Python: retry_after) in seconds
5xx ServerError, with requestId (Python: request_id) to quote to support

ConnectionFailedError means the request got no answer, and RequestTimeoutError that the answer didn’t come in time. ConfigError means the client couldn’t start: no token, or an address it won’t send one to.

Reads, and changes that are safe to repeat, are tried again after a 5xx, a timeout or a lost connection: twice by default, with a short wait that grows each time. Set maxRetries (Python: max_retries) to change that. After a 429 they wait as long as its Retry-After says, up to a minute, and go again.

A change that isn’t safe to repeat, like making an app or a token, is never sent twice. After a 429 it throws RateLimitError, with how long to wait. A deploy is never sent twice either: when its answer doesn’t come, or after a 429, the client looks in the version list for the version it was making, made of the same files, and follows that one. A teammate’s deploy at the same moment isn’t taken for yours.