# Java SDK One Java client for every Runtime Cloud product. `com.withruntime:withruntime` needs Java 17 or later and nothing beyond the JDK: no HTTP library, no JSON library. It is a named module, `com.withruntime`. ```xml com.withruntime withruntime 0.31.1 ``` With Gradle: `implementation("com.withruntime:withruntime:0.31.1")`. **The client finds its key by itself.** It uses `RUNTIME_API_KEY` when that is set, and otherwise the connection this machine saved (any `npx withruntime` command connects it, with one browser approval). On a server, put a key from https://withruntime.com/account/keys in `RUNTIME_API_KEY`, stored in your secret manager. Never put it in source code, a URL or a command-line argument. With no key anywhere, `RuntimeClient.create()` throws `RuntimeCloudException.Authentication` whose code is `missing_api_key` and whose hint says how to get one. ## Hello, sandbox ```java import com.withruntime.*; public class Hello { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create(new CreateSandbox())) { sbx.files().write("/workspace/invoice.py", "print(sum([125, 250, 375]))\n"); CommandResult result = sbx.exec("python3 /workspace/invoice.py", new ExecOptions().check(true)); System.out.print(result.stdout()); } } } ``` `create` returns once the sandbox is running. Closing the sandbox deletes it. `new CreateSandbox().keep()` makes closing stop the sandbox instead, and keeps its disk. `delete()` deletes a sandbox at any time. With no options, you get the free usage while it lasts, the default region, and 2 vCPU, 4 GiB of memory and a 4 GiB disk. The sandbox runs while it works and pauses itself when idle. It has no time limit unless you set one with `timeoutSeconds(seconds)` (60 to 86,400). The free usage is spent first, then prepaid credit. `funding(...)` is accepted and ignored. Every field is optional: ```java import com.withruntime.*; public class Sized { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); Sandbox sbx = runtime .sandboxes() .create( new CreateSandbox() .name("tests-42") .label("team", "search") .vcpu(2) .memoryMiB(4096) .diskMiB(8192) .timeoutSeconds(900) .onLeaseEnd("stop") .network(new NetworkRules().internet(true).allow("repo.maven.apache.org"))); System.out.println(sbx.id() + " " + sbx.info().funding() + " " + sbx.info().endsAt()); sbx.stop(); } } ``` A field newer than the SDK goes in with `.set("field", value)`. ## Run commands `exec` runs a string under `bash -c`. `execArgv` runs a program directly, with no shell. Use it for untrusted arguments. ```java import com.withruntime.*; import java.time.Duration; import java.util.List; public class Commands { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { sbx.exec("mkdir -p app && echo 'print(1 + 1)' > app/main.py"); CommandResult run = sbx.execArgv( List.of("python3", "main.py"), new ExecOptions() .cwd("/workspace/app") .env("API_TOKEN", System.getenv("API_TOKEN")) .timeout(Duration.ofMinutes(2))); if (run.exitCode() != 0) System.err.print(run.stderr()); System.out.print(run.stdout()); sbx.exec("for i in 1 2 3; do echo line $i; sleep 1; done", new ExecOptions().onStdout(System.out::print)); try (EventStream events = sbx.execStream("node --version", new ExecOptions())) { for (OutputEvent event : events) if (event.type().equals("stdout")) System.out.print(event.data()); } } } } ``` - `env` passes secrets to a command. Runtime never echoes them back. Never put a secret in the command line. - `stdin` gives the command input, then closes it. - `.user("root")` runs the command as root, with no `sudo` in front, keeping its `env` and `PATH`; `HOME` is `/root`. `SpawnOptions` takes it too, and `ProcessInfo.user()` says which user a process runs as. - The default timeout is 60 seconds; the maximum is 24 hours. A timeout is a result (`timedOut()` is true, with the output so far), not an exception. - `check(true)` throws `RuntimeCloudException.Command`, with the exit code and output, on a non-zero exit or a timeout. - A result holds all of the output, up to 256 MiB of each stream (no limit with `onStdout` or `onStderr`). `stdoutBytes()` and `stderrBytes()` give the bytes each stream wrote. `maxOutputBytes(n)` keeps at most `n` bytes of each stream; `truncated()` is then `true`. With `onStdout`, `onStderr`, or a timeout over a minute, the command streams. When the server ends a long stream, it resumes by itself. No output is dropped. ## Background processes ```java import com.withruntime.*; public class Processes { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { SandboxProcess server = sbx.spawn("python3 -m http.server 8000", new SpawnOptions().cwd("/workspace")); SandboxProcess repl = sbx.spawn("python3 -i -q", new SpawnOptions().pipeStdin()); repl.write("print(21 * 2)\n", true); System.out.print(repl.waitFor().stdout()); for (ProcessInfo process : sbx.processes()) System.out.println(process.id() + " " + process.state()); server.kill("SIGTERM"); } } } ``` A process outlives your connection. Get it back with `sbx.process(id)`. `write` tracks input offsets, so a retried write is never typed twice. ## Files ```java import com.withruntime.*; import java.nio.file.Path; public class FilesExample { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { sbx.files().write("/workspace/data/input.csv", "a,b\n1,2\n"); System.out.print(sbx.files().readText("/workspace/data/input.csv")); for (FileEntry entry : sbx.files().list("/workspace", 2, null)) System.out.println(entry.type() + " " + entry.size() + " " + entry.path()); sbx.files().mkdir("/workspace/out", true); sbx.files().rename("/workspace/data/input.csv", "/workspace/out/input.csv", false); sbx.files().upload(Path.of("src"), "/workspace/project/src"); sbx.files().download("/workspace/out", Path.of("out")); } } } ``` `write` creates parent directories and replaces the file atomically. A file over 1 MiB goes in parallel chunks, each checked by SHA-256. `stat` returns `null` for a path that does not exist. `upload` and `download` move a whole directory as one gzipped tar archive, through the API's folder routes. The sandbox packs and unpacks it with `tar` ([files in the API guide](./api#files)). A large folder uploads in parts. A downloaded folder is unpacked as it arrives. It lands in place only after all of it has arrived. A download cut short fails with `download_incomplete` and writes nothing. ## Pause, wake, fork and snapshot ```java import com.withruntime.*; import java.time.Duration; import java.util.List; public class Lifecycle { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create(new CreateSandbox())) { sbx.pause(); // memory and files are kept; compute billing stops Sandbox again = runtime.sandboxes().get(sbx.id()); again.wake(Duration.ofMinutes(20)); again.extend(Duration.ofMinutes(10)); List copies = again.fork(new ForkOptions().count(2)); for (Sandbox copy : copies) copy.stop(); Snapshot snapshot = again.snapshot(new SnapshotOptions().name("ready").retentionDays(7)); try (Sandbox fromSnapshot = runtime.sandboxes().create(new CreateSandbox().snapshot(snapshot.id()))) { System.out.println(fromSnapshot.id()); } runtime.snapshots().delete(snapshot.id()); } } } ``` A fork copies a sandbox as it is now, with its files, memory and running processes. It runs on its server while that server has room, and on another server when it is full. See [JavaScript](./javascript) for what forks and snapshots keep and how they are billed. If a copy fails, the exception's `details().get("startedSandboxIds")` names the copies that did start. `wake(Duration)` and `extend(Duration)` set and move a time limit, for a sandbox that has one. On a sandbox without a time limit, `extend` changes nothing. `sbx.keepAlive(every, margin, onError)` moves the time limit on from your process until `stop()`. `runtime.sandboxes().getOrCreate(name, options)` returns the sandbox with that name, woken or restarted, or creates it. ## Share a port ```java import com.withruntime.*; public class Share { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { sbx.spawn("python3 -m http.server 3000", new SpawnOptions()); Preview preview = sbx.previews().create(3000); System.out.println(preview.url() + " " + preview.token()); // send the token as x-runtime-preview-token } } } ``` A preview is private by default. `create(port, "public", null)` shares it with anyone who has the address. Addresses are under `runtimehost.com`. ## Images, volumes, network rules and secrets ```java import com.withruntime.*; import java.util.List; import java.util.Map; public class Storage { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); Image image = runtime .images() .build( new CreateImage().name("data").recipe(Map.of("pip", List.of("pandas"), "apt", List.of("jq"))), line -> System.out.println(line.getString("text"))); Volume volume = runtime.volumes().create(10240, "cache", null); try (Sandbox sbx = runtime.sandboxes().create(new CreateSandbox().image(image.id()).volume(volume.id(), "/data", null))) { System.out.print(sbx.exec("python3 -c 'import pandas; print(pandas.__version__)'").stdout()); JsonObject cell = sbx.interpreter().run("import math\nround(math.pi * 2, 3)"); System.out.println(cell.getObjects("results").get(0).getObject("data").getString("text/plain")); sbx.network().set(new NetworkRules().internet(true).allow("pypi.org", "*.pythonhosted.org")); } JsonObject secret = runtime.secrets().set("OPENAI_API_KEY", "sk-...", List.of("api.openai.com")); System.out.println(secret.getString("placeholder")); // what sandboxes see in $OPENAI_API_KEY } } ``` `images().build` waits until the image is ready. It throws with code `image_failed` when the build fails. `images().create` queues the build and returns. A Dockerfile build takes `dockerfile(text)` and `contextDir(path)`, the folder `docker build` would read, with its `.dockerignore`. Only the parts the server does not have yet are uploaded. See [custom images](./images). A volume lives on one server. Its backups can restore a new volume onto another host. `sbx.desktop()` drives a Linux desktop in the sandbox. ## Metrics, events and webhooks ```java import com.withruntime.*; import java.nio.charset.StandardCharsets; import java.util.List; public class Hooks { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); JsonObject hook = runtime.webhooks().create("https://example.com/hooks/runtime", null, null); String secret = hook.getString("secret"); // shown once; keep it // In your endpoint, with the raw body and the Runtime-Signature header: byte[] body = "{}".getBytes(StandardCharsets.UTF_8); String header = "t=0,v1=0"; try { JsonObject event = Products.Webhooks.verify(body, header, List.of(secret)); System.out.println(event.getString("type")); } catch (SecurityException forged) { System.out.println("refused: " + forged.getMessage()); } for (JsonObject event : runtime.events().list(null, "sandbox.stopped")) System.out.println(event.getString("createdAt") + " " + event.getString("type")); } } ``` `sbx.metrics("15m")` returns the sandbox's measured CPU and memory, and `runtime.otel().create(...)` pushes events and metrics to an OpenTelemetry endpoint. See [metrics and webhooks](./observability). ## Errors and retries ```java import com.withruntime.*; public class Errors { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try { runtime.sandboxes().get("00000000-0000-4000-8000-000000000000"); } catch (RuntimeCloudException.NotFound missing) { System.out.println("no such sandbox"); } catch (RuntimeCloudException failure) { System.out.println(failure.code() + " " + failure.hint() + " " + failure.requestId()); } } } ``` Every exception is a `RuntimeCloudException` (unchecked) with `code()`, `getMessage()`, `hint()`, `requestId()`, `status()` and `details()`. Its subclasses are `Authentication`, `PermissionDenied`, `NotFound`, `Conflict`, `InvalidRequest`, `RateLimited`, `ServiceUnavailable`, `Connection` and `Command`. They are the same classes as in the JavaScript, Python and Go SDKs. `no_capacity` is `ServiceUnavailable`. `no_credit_running_limit`, and a `quota_exceeded` on what runs at the same time, are `RateLimited`. A `quota_exceeded` on what is kept is `Conflict`. Every write carries an idempotency key. The client makes it for you and keeps it across its own retries. Transport failures, and every response that says `retryable: true`, are retried after its `Retry-After`. Retries stop at the maximum number of retries. Each retry uses the same key, so a retry never makes two sandboxes or runs a command twice. A wait over 60 seconds is not slept on. The error returns immediately, with `retryAfterMs`. A response that ran nothing (`guest_busy`, `busy`, `rate_limited`, `sandbox_starting`) is sent again until the call's deadline. Without a deadline, it is sent again for five minutes, past the maximum. `retryable()` says whether trying the same call again may work. **A create, wake or restart waits for room.** When all four slots without credit are taken (`no_credit_running_limit`), the account is at its limit (`quota_exceeded`), or the region is full (`no_capacity`), `sandboxes().create` waits and sends the same request again, for up to two minutes. Set `waitForCapacity` on the client builder, or on one `CreateSandbox`, wake or restart. Zero fails immediately. A sandbox stays on its server, so a wake or restart refused with `no_capacity` is sent again with the same idempotency key until room frees. A fork waits the same way when no server has room for a copy; set `waitForCapacity` on its `ForkOptions` to change the wait. An owner, admin or developer can make a read-only key and set a daily spending limit on a key at [API keys](https://withruntime.com/account/keys). `runtime.limits().get()` reads both. Past the limit, a create, wake or extension throws with code `spending_limit_reached`, and it is not retried. ## Configuration ```java import com.withruntime.*; import java.time.Duration; public class Configured { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.builder() .apiKey(System.getenv("MY_RUNTIME_KEY")) .timeout(Duration.ofMinutes(2)) .maxRetries(4) .waitForCapacity(Duration.ofMinutes(10)) .build(); System.out.println(runtime.me().orgId()); System.out.println(runtime.request("GET", "/v1/volumes", null, null)); // any endpoint, same rules } } ``` - `RUNTIME_API_URL` points the client at another API origin, as `baseUrl` does. - Code inside a Runtime sandbox calls Runtime's API at `http://runtime.internal` ([Runtime's API from inside a sandbox](./sandbox-environment#runtime-s-api-from-inside-a-sandbox)). - The client is safe for concurrent use. Make one and share it. It keeps its connections open and holds at most 32 calls in flight (`maxConnections`). - It honours `HTTPS_PROXY` and `NO_PROXY`. `httpClient` supplies your own `java.net.http.HttpClient`. - Money is integer microdollars: 1,000,000 is one US dollar. `runtime.usage()` returns the account's balance exactly, as `BigInteger`. - `runtime.usageExport(since, until, cursor, limit)` reads one page of settled usage over a UTC range, with its server CSV. Follow `nextCursor()` until it is null. `finalThrough()` is the time before which no more charges will settle. A row's `isFinal()` is true once its resource is deleted and can be charged nothing more ([export settled usage](./api#export-settled-usage)). ## Network products and backups `runtime.domains()`, `ports()`, `addresses()` and `tunnel()` manage account networking. Domains and ports name the sandbox they serve. `tunnel()` is the account's WireGuard network. It is separate from a sandbox's TCP forwarding. Responses keep the API's complete fields through `JsonObject`. Use `getLong` for integer money, and `getInstant` for funding deadlines. When credit runs out, an address or tunnel reservation stays until it is released. Traffic stops during the unfunded time. ```java import com.withruntime.*; import java.util.List; public class NetworkProductsExample { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); JsonObject domain = runtime.domains().add("app.example.org", args[0], 8080); System.out.println(domain.getObjects("records")); // Publish these DNS records, then verify. JsonObject address = runtime.ips().reserve(4); System.out.println(address.getString("address")); System.out.println(address.getLong("rateMicros")); runtime.networks().wireguard().create("10.66.0.0/24"); JsonObject peer = runtime.networks().wireguard().addPeer("laptop", null, List.of()); // Save peer.getString("config") privately, even when configReady is false. // Its private key is shown once; generated-key calls are not automatically retried. System.out.println(peer.getBoolean("configReady")); System.out.println(runtime.sso().get()); // Owner console manages SSO settings. } } ``` `volumes().backup`, `backups`, `getBackup`, `deleteBackup`, `setBackupPolicy` and `restore` manage backups. `setBackupSchedule(id, 60, null, Map.of("hourly", 24, "daily", 7))` backs the volume up every 60 minutes (or pass a UTC cron instead of the minutes) and keeps backups by those counts. `clearBackupSchedule(id, null)` goes back to daily. A restore creates a new volume. It does not replace the original. A backup's `backedUp` field is true once its copy has been checked off the source host. Writes after that backup are not in it. ```java import com.withruntime.*; public class VolumeBackupExample { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); runtime.volumes().setBackupPolicy(args[0], true, 7); JsonObject backup = runtime.volumes().backup(args[0], "before-update"); if ("ready".equals(backup.getString("state"))) { Volume restored = runtime.volumes().restore(backup.getString("id"), "restored"); System.out.println(restored.id()); } } } ``` ## File permissions and watching `Files.WriteOptions.mode(0755)` sets permissions atomically on current guest images. `0` means no permissions, rather than the default. Older images use the `exec` permission for a chmod fallback. A supplied write idempotency key covers the whole upload, including completed retries. `files().chmod` changes an existing file's permissions. A watch's `events()` follows its cursor across server handovers. Loss notices (`overflow` or `lost`) mean the caller should rescan. Closing the event stream stops reading. `watch.stop()` also stops the guest watch. A later `events()` resumes after a pause or an earlier reader close. Only one reader uses a watch at a time. `files().watches()` also lists watches, polls and stops them by ID. ```java import com.withruntime.*; import java.util.Map; public class FileWatchExample { public static void main(String[] args) { Sandbox sandbox = RuntimeClient.create().sandboxes().get(args[0]); sandbox.files().write("/workspace/run.sh", "#!/bin/sh\necho ready\n", new Files.WriteOptions().mode(0755).idempotencyKey("install-run-script-v1")); FileWatch watch = sandbox.files().watch("/workspace", Map.of("recursive", true)); try (EventStream events = watch.events()) { for (JsonObject event : events) { System.out.println(event); if ("paused".equals(event.getString("k"))) break; } } finally { watch.stop(); } } } ``` ## Terminals and TCP forwarding A terminal carries binary input/output over an authenticated WebSocket. Read output while writing input. Close it to release a blocked reader or writer. Interrupting its reader also closes it. Its opening deadline follows the client's timeout. Closing an attached terminal does not stop its existing process. A new terminal's shell ends with the connection. `openTunnel()` multiplexes loopback TCP streams. A stream's `closeWrite()` sends input EOF and keeps reading the response. Each stream holds at most 1 MiB of unread response data. If a stream exceeds that bound, only that stream fails with `tunnel_receive_overflow`, after delivering its buffered bytes. `portForward()` copies both directions for each local connection. It closes its listener and active connections together. ```java import com.withruntime.*; import java.net.InetSocketAddress; import java.util.Map; public class TerminalForwardExample { public static void main(String[] args) throws Exception { Sandbox sandbox = RuntimeClient.create().sandboxes().get(args[0]); try (Terminal terminal = sandbox.terminal(Map.of("command", "printf ready"))) { terminal.input().transferTo(System.out); System.out.println(terminal.exitCode()); } try (PortForward forward = sandbox.portForward(5432, new InetSocketAddress("127.0.0.1", 0))) { System.out.println(forward.address()); // Connect your local database client here. System.in.read(); } } } ``` ## Bucket mounts and recordings `sbx.mounts().add(options)` takes provider, bucket, path and the name of a Runtime secret, keeping the bucket key outside the sandbox. `list()` and `remove(path)` manage its lifecycle. `sbx.desktop().recordings()` has `start`, `get`, `list`, `stop`, `download` and `delete`. Downloads return MP4 bytes. Recordings live on the sandbox's own disk and count toward that disk's space.