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.
Install
Section titled “Install”npm install @jiayang-cloud/clientNode 20 or newer, Deno, Bun and Workers. No dependencies.
pip install jiayang-clientPython 3.10 or newer. Its one dependency, truststore, has it trust what your operating system trusts, as the CLI does.
Sign in
Section titled “Sign in”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));from jiayang_client import Client
jy = Client()me = jy.me()print(me["email"], [w["slug"] for w in me["workspaces"]])The client looks for a token in this order:
- The token you pass:
new Jiayang({ token })orClient(token). JIAYANG_TOKEN.- The session
jiayang loginsaved, 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.
Deploy
Section titled “Deploy”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`);from pathlib import Path
version = jy.deploy.site("acme", "handbook", [ {"path": "/index.html", "content": "<h1>Handbook</h1>"}, {"path": "/logo.png", "content": Path("logo.png").read_bytes()},])print(f"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") } },});jy.deploy.worker( "acme", "hello", main_module="index.js", compatibility_date="2026-09-01", modules={"index.js": {"content": Path("dist/index.js").read_text()}},)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 });scan = { "lockfiles": [{"path": "package-lock.json", "text": Path("package-lock.json").read_text()}], "allow": Path(".jiayang-allow").read_text(),}jy.deploy.site("acme", "handbook", files, scan=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");jy.access.share("acme", "hello", "ada@example.com", role="editor")jy.access.share_domain("acme", "hello", "acme.com")jy.access.share_group("acme", "hello", "finance")
still = jy.access.unshare("acme", "hello", "ada@example.com")["still_shared_by"]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.
Read the audit log
Section titled “Read the audit log”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);}for entry in jy.audit.iterate("acme", app="hello", decision="deny"): print(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.
Bypass tokens
Section titled “Bypass tokens”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);made = jy.tokens.create("acme", "hello", "nightly", "viewer", expires_in_days=30)# Send made["secret"] as `Authorization: Bearer <secret>` to made["url"]. It is shown once.
jy.tokens.revoke("acme", "hello", made["token"]["id"])A revoked token stops working at once. See Tokens for scripts and machines.
Everything else
Section titled “Everything else”| 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.
Errors
Section titled “Errors”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.
Retries
Section titled “Retries”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.
Next steps
Section titled “Next steps”- CLI reference: the same things, from a terminal.
- Deploy with GitHub Actions: deploys on every push.
- Tokens for scripts and machines: calling your apps from code.