---
title: "Java SDK"
description: "The Java SDK, one client with a namespace for each product (Cube, Sandbox, Browser, Drive, Database) and the platform's own calls. Java 17 and later, on the JDK's own java.net.http, with no dependencies."
---

> Documentation Index
> Fetch the complete documentation index at: https://glac.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Java SDK

## Install

Java 17 or later, on the JDK's own `java.net.http` (HTTP and WebSocket); no dependencies, no framework. Calls block, errors throw `GlacException` (unchecked), returned objects are records, options are chained `…Options`, durations are `Duration` and times `Instant`. It is on Maven Central as `ai.glac:glac`, in the package `ai.glac`:

```xml
<dependency>
  <groupId>ai.glac</groupId>
  <artifactId>glac</artifactId>
  <version>0.1.0</version>
</dependency>
```

## The client

```java
Glac glac = new Glac(); // GLAC_API_KEY and GLAC_API_URL; or Glac.builder().apiKey(key).baseUrl(address).build()

App app = glac.cube().deploy("ghcr.io/acme/api:1.4", new AppOptions().name("api").port(8080)); // Cube: waits until it serves
System.out.println(app.url());

Sandbox sbx = glac.sandboxes().create(); // Sandbox: stays until deleted
Result res = sbx.run("python3 -c 'print(6*7)'"); // res.stdout(), res.stderr(), res.exitCode()
System.out.println(res.stdout() + " " + sbx.url(3000));

Drive drive = glac.drives().create(new DriveOptions().name("data").sizeBytes(10L << 30)); // Drive

System.out.print(Sandbox.runOnce("echo hello").stdout()); // the shorthand: a sandbox for one command, on the default client
```

`glac.cube().deploy` is `glac deploy`: a new version of the app called `api`, which is made first where there is none, waited for until it has the app's traffic. The last line is the one shorthand there is, the sandbox SDKs' one-liner without a client. Nothing deletes anything but the one-off sandbox and your own `delete()`; `Sandbox` does not implement `AutoCloseable`, since a sandbox is meant to stay.

`new Glac()` reads the key from `GLAC_API_KEY` and the address from `GLAC_API_URL` (default `https://api.glac.ai`). For another address, key or timeout, build a client:

```java
Glac glac = Glac.builder().apiKey(key).baseUrl(address).build();
Sandbox named = glac.sandboxes().create(new CreateOptions().name(name).env("STAGE", "dev").neverSleep());
```

| Call | What it does |
|---|---|
| `new Glac()`, `Glac.builder()`: `apiKey`, `baseUrl`, `timeout`, `connectTimeout`, `retries`, `onResponse` | The address, the key, the timeouts and the retries. Any number of threads may share a client. |
| `Glac.shared()` | The client the shorthand uses: made from the environment on first use, as `new Glac()` is. |
| `glac.cube()` | Cube: apps (below). `glac.workers()` is another name for it, marked `@Deprecated`. |
| `glac.sandboxes()`, `.snapshots()`, `.sshKeys()` | Sandboxes, their snapshots, and the SSH keys that open them (below). |
| `glac.drives()`, `glac.volumes()`, `glac.backups()` | Drives, a control plane's, with their snapshots; the volumes underneath them; and the backups of volumes, a control plane's (below). |
| `glac.domains()` | The project's own domains, a control plane's (below). |
| `glac.webhooks()`, `glac.keys()` | Where the project's events are sent, and its API keys, a control plane's (below). |
| `glac.destinations()` | Where the logs, metrics and traces of the project's apps are exported, a control plane's (below). |
| `glac.secrets()`, `glac.variables()` | The project's secrets and shared variables (below). |
| `glac.deploys()` | The project's deploys (`/v1/deploys`, a control plane's): several of its services deployed together, in the order of what each waits for, and switched together ([a project's deploy](/docs/cube/services/#deploying-a-project)). `create(Map.of("api", new DeployOptions().image("ghcr.io/acme/api:2"), "web", new DeployOptions()))` gives each service what a deploy of it alone takes and waits (`Deploy.await()`), following a deploy that took its place; one that fails throws a `GlacException` of type `deploy_failed` with the deploy's code (`job_failed`, `job_timed_out`, `job_lost`, …) and the service as `param()` (`services.<name>`). `create(services, new ProjectDeployOptions().noWait())`, `get(id)`, `list()`. |
| `glac.account()`, `updateAccount`, `usage`, `usageReport`, `billing`, `updateBilling`, `balance`, `updateBalance`, `topUp(usd)`, `charges`, `ledger`, `project`, `events` | The platform's own calls, which belong to no product (below). |
| `glac.request("GET", "/v1/webhooks", null, null)` | An API the SDK does not wrap: its answer's JSON. |

The namespaces are methods returning an object `Glac` makes, one per client. The objects keep the API's names: a Cube is an `App` (the API calls it an app), and `Sandbox`, `Drive`, `DriveSnapshot`, `Volume`, `Backup`, `Domain`, `Snapshot`, `SshKey`, `Secret`, `Variable`, `Version`, `Instance`, `Deploy`.

## Cube

A Cube, which the API calls an app, is an image that runs as a service at an address of its own, with as many instances as its requests need and none while there are none. Every deploy makes a version, which takes the app's traffic once it is ready. Apps are a control plane's: against a node that stands alone every call is the node's own 404 `no_such_route`.

| Call | What it does |
|---|---|
| `glac.cube().deploy(image)`, `deploy(image, new AppOptions().name("api")…)` | As `glac deploy`. Of a name the project has no app of: the app is made with every option given, and its first version with it. Of an app that is there: what holds at once and is given (regions, scale, keys, egress, logs, traces, …) is changed first (`PATCH`, only when something of it is given), then a version is made of what is given and, for what is not, of what the app has (`POST …/versions`). A name somebody else takes in between (409 `name_taken`) is a deploy to their app; no name is a new app the API names. Waits for the version unless `noWait()`, and returns the app looked at again (`url()`, `currentVersion()`). |
| `glac.cube().create(AppOptions)`, `get(idOrName)`, `getOrCreate(name, AppOptions)`, `list()` | Makes an app (with an image, its first version is waited for); finds one; finds or makes one (one that is there is returned as it is); lists. `drives(List.of(new DriveMount(…)))` are the Drives its one instance mounts read-write (an app of many instances or of keys is refused, 409 `drive_single_writer`; read-only snapshot mounts are a sandbox's, and a mount with a snapshot is refused there, 400 `unknown_field`); `keyDrives(List.of(App.KeyDrive.of(name, path)))` give each key's instance of a keyed app Drives of its own, made with its first instance and deleted with the key. |
| `glac.cube().logs(idOrName, LogsOptions)` | An app's log by its id or name, without fetching the app. |
| `app.deploy(new DeployOptions().image(…))`, `app.update(AppChanges)`, `app.rollback("1")`, `app.versions()`, `app.delete()` | A version of the app in hand; a change; the traffic back to a version; its versions; delete. |
| `.runtime(Map.of("ulimits", Map.of("nofile", 65535)))` of `AppOptions` and `DeployOptions` | The version's [runtime settings](/docs/cube/versions/#runtime-settings), as the API takes them: `ulimits`, `sysctls`, `tmpfs`, `dns`, `shm_size_bytes`; replaced whole. |
| `app.logs(new LogsOptions().follow())`, `app.logUsage(…)`, `app.deleteLogs(before)`, `app.info().logsState()` | The log (an `Iterable<LogLine>` that holds on through lost connections and a control plane that restarts); what it counted; delete lines; whether each machine that keeps it keeps what is printed there now for the long term ([logs](/docs/platform/logs/#what-is-kept-and-for-how-long)). |
| `app.instance(key, region)`, `app.getInstance(key)`, `app.instances()`, `app.deleteInstance(key)` | Keyed instances: one instance per key, made or woken. |
| `instance.upgrade("3")`, `app.upgradeInstance(key, version)`; `upgrade.await()`, `await(limit)`; `instance.upgrades()`, `app.instanceUpgrades(key)`, `app.instanceUpgrade(key, id)` | Moves a key's instance to another version of the app, [in place](/docs/cube/keyed-instances/#a-new-version-for-one-instance): `null` is the app's current version, else its id or its number (one that is ready, an older one too). It keeps its id, its node, its key, its address, its tokens and its own Drives; its disk, its machine and its processes become the new version's, and its requests are held at the edges while it is switched. Returns the `Upgrade` under way (its `info()`: `status` `preparing`, `switching`, `done` or `failed`, `fromVersion`, `toVersion`, `timings`, `error`, `putBack`, `kind` (`upgrade`, or `revert`), `when`, `keepDays`, `rollback` (what a revert of it needs, kept for `keepDays`), `reverts`, `batch`; an upgrade asked through the API to wait for its instance to sleep is `waiting` until then, and `cancelled` where it was cancelled (the API's `DELETE`, or its batch stopped): one that was cancelled is over, and its wait raises as one that failed does, with the code `cancelled`), or `null` where it is on that version already; `await()` throws a `GlacException` of type `upgrade_failed` (its code `upgrade_failed` or `upgrade_not_here`) where the new version did not come up, the instance on the version it was on. |
| `app.upgradeInstanceWith(key, new UpgradeOptions().version("4").whenAsleep().keepDays(14))`, `instance.upgradeWith(…)` | The same with `whenAsleep` (it waits, `waiting`, until the instance sleeps and begins then, so that none of its requests is held) and `keepDays` (0 to 365, 7 unless given: how long what a revert needs is kept once it is done, billed as snapshots are). A version whose services mount other Drives or volumes than the instance's is no upgrade in place: it ends `failed` with `upgrade_not_here`. |
| `upgrade.cancel()`, `app.cancelInstanceUpgrade(key, id)`; `upgrade.revert()`, `upgrade.revert(keepDays)`, `app.revertInstanceUpgrade(key, id, keepDays)` | An upgrade that waits (for its instance to sleep, or for its turn in a batch) cancelled: `cancelled`, the instance on its version (one that has begun is 409 `upgrade_started`). A [revert](/docs/cube/keyed-instances/#a-new-version-for-one-instance): the instance back to what an upgrade kept, its disk, its Drives as they were before it (a snapshot of what they hold now made first) and the version it came from; a new `Upgrade` of kind `revert`, which goes and is awaited as an upgrade is. Refused 409 `rollback_gone` where what it goes back to is not kept any more. |
| `app.upgradeInstances(new UpgradeBatchOptions().version("4").fromVersions(List.of("3")).keys(List.of("k")).whenNow().keepDays(1).concurrency(8).maxFailures(1))`; `app.upgradeBatches()`, `app.upgradeBatch(id)`, `app.stopUpgradeBatch(id)`; `batch.await()`, `await(limit)`, `batch.refresh()`, `batch.stop()` | A batch of upgrades of the app's keys to one version (the app's current one unless given): each once its instance sleeps (or as its turn comes, `whenNow`), 4 at once unless given (two on one node), stopped `failed` after 5 failures unless given. An `UpgradeBatch` whose `info()` is an `UpgradeBatch.Info`: `status` (`running`, `done`, `failed`, `stopped`), `stopReason`, `counts`, `upgrades` (each member's key, upgrade, status and error), `skipped`, `origin`. `await()` returns it however it ended; a member whose instance never sleeps keeps it running, so `await(limit)` throws `wait_timeout`. `stop()` cancels the members that wait. |
| `app.openInstancePort(key, "tcp", port, "2001:db8::/32")`, `app.instancePorts(key, "tcp")`, `app.closeInstancePort(key, "tcp", port)` | The ports open at the own public IPv6 address of a key's instance, wherever it is: its sandbox's `openPort`, `ports` and `closePort`, by the app and the key (`"udp"` for UDP; [keyed instances](/docs/cube/keyed-instances/#its-own-ipv6-address)). An instance with no address of its own is refused (409 `no_ipv6`). |
| `checkpoint(n)` of `AppOptions` and `AppChanges`, `AppChanges.noCheckpoint()` | [Checkpoints](/docs/cube/keyed-instances/#checkpoints) of the app's keyed instances while they are awake (1 to 600; 0 is the default, 10), and none again; `App.Info.checkpoint()` says it. An instance's `SandboxInfo.checkpoint()` (a `SandboxInfo.Checkpoint`: `everySeconds`, `lastAt`, `behindSeconds`, `bytesPerSecond`, `storedBytes`, `error`; null without them) says how its checkpoints go. |
| `app.metrics(from, to, step)` | What the machines that serve the app counted of it, a step a minute (`"hour"`: an hour; nulls: the last hour by the minute): requests by the class of their status, how long they took, its instances and what they used ([metrics](/docs/platform/logs/#metrics)). The API's object. |
| `traceSampleRate(0.1)`, `trustIncomingTrace(true)` of `AppOptions` and `AppChanges`; `app.info().traces()` | A span of the platform's for a share of the app's requests, which the instance's own spans are children of (none unless given), sent to the project's destinations that take `"traces"` ([traces](/docs/platform/logs/#traces)); what the app has (an `App.TraceSettings`). |
| `app.createKeySecret()`, `app.keySecrets()`, `app.keySecret(id)`, `app.deleteKeySecret(id)`, `KeySecret.signKey(secret, appId, key, expiresAt)` | The secrets an app's keys are signed with (a `KeySecret`, whose `secret()` only the one just made has), and a key signed with one, as a request through the edge carries it ([signed keys](/docs/cube/keyed-instances/#signed-keys)); `KeySecret.pathSegment` writes it as a path segment. |

A version that does not come up throws a `GlacException` with `type()` `deploy_failed` and the version's own code and words; the traffic stays where it was. What a version is made of (`image`, `startCommand`, `port`, `env`, `secret`, `vcpus`, `memoryBytes`, `volumes`, `drives`, `keyDrives`, …) is the whole of a `DeployOptions`; the rest of what an app has holds at once and makes no version.

## Sandbox

| Call | What it does |
|---|---|
| `glac.sandboxes().create(new CreateOptions().image(…).name(…).env(k, v).drive(new DriveMount("/data", id)).sleepAfter(d).neverSleep().sleepNotice(d).sleepHoldMax(d).egress(…))` | Makes a sandbox; every option may be left out. A service started when it boots: `start(true)` (the image's own start command), `startLine(…)` or `startArgv(…)`, with `readyTcp(port)`, `readyHttp(port, path)` or `readyCommand(…)`; `hostname(…)`, `stopSignal(…)` and `stopGrace(d)`; `projectNetwork()` puts it on the project's private network from the start (`info().projectAddress()`); `new VolumeMount("/data", vol).withCopy()` gives a new volume what the image has at its path ([making a sandbox](/docs/sandbox/commands/#making-a-sandbox)). |
| `sbx.info().appId()`, `versionId()`, `key()`, `imageDigest()` | A sandbox made through the control plane is an instance of an app: the project's default sandbox app (`sbx-sandbox`) when it was made with no image, else the app of its image (the app's `instances()` lists that app's sandboxes, each under its key); the version of that app it is of, the image it was made of (a tag that was moved since is a new version of the app, which the sandboxes made after it are of); its key there, its name or its id; and its image by digest. Said by its create's answer and by every read of it, and kept; `null` for one made from a template or a snapshot, and for a fork. |
| `glac.sandboxes().get`, `getOrCreate`, `list(ListOptions)`, `listPage(ListOptions)` | Finds; finds or makes; lists (all, or a page). |
| `suspendAfter(d)`, `wakeDailyAt("03:30")` of `CreateOptions` and `UpdateOptions` (`defaultSuspendAfter()`, `noDailyWake()` take them back); `SandboxInfo.suspendAfter()`, `wakeDailyAt()` | How long it stays paused, once idle has stopped it, before its memory is saved and it sleeps for real (a second to a day; its node holds it to ten minutes at the most), and a time of day in UTC at which it is woken every day while it rests, for work of its own that waits for a wake. |
| `onExpiry("sleep")`, `heldAsleep(true)` of `CreateOptions` and `UpdateOptions`; `SandboxInfo.onExpiry()`, `heldAsleep()` | What `expiresAt` does when it comes: `delete` the sandbox (the default), `sleep` (put it to sleep and keep it, a request waking it as ever) or `hold` (the same, held asleep); and whether a request to one of its ports wakes it while it sleeps: held asleep, its preview address answers 502 `sandbox_paused`, and a call of the API wakes it. |
| `glac.sandboxes().run(command)`, `run(command, RunOptions, CreateOptions)` | A sandbox for one command: made, run, deleted. |
| `CreateOptions.init("systemd")`; `SandboxInfo.init()` | A whole system: the image's own systemd is the sandbox's PID 1 (without an image, the platform's Ubuntu with systemd and OpenSSH), so packages, services started with `systemctl`, cron and the journal work as on a machine, and it has a machine ID of its own, kept through sleeps, wakes and moves. Not with a start command or a ready check (400 `invalid_start`), nor from a snapshot. `init()` is `"systemd"` for one and null for the platform's init. |
| `sbx.run(command, new RunOptions().cwd(…).timeout(d).onStdout(…))` | Runs and waits. `Result` has `stdout`, `stderr`, `exitCode`, `duration`, `usage`, `wake`. |
| `sbx.exec`, `sbx.terminal()` | Starts a command (`waitFor()`, `output()`, `write()`, `kill()`); a terminal. |
| `sbx.files()` | `read`, `readText`, `open`, `write`, `list`, `stat`, `exists`, `mkdir`, `move`, `remove`, and `setAttributes(path, mode, sizeBytes, modifiedAt)`: a file's mode, its length, its time of last modification (null leaves one as it is); `.asUser("node")`. |
| `sbx.url(port)`, `sbx.token(scopes, expiresIn)` | A link to a port (signed locally); a token for this sandbox. |
| `sbx.info().ipv6()`, `sbx.openPort("tcp", port, from…)`, `sbx.ports("tcp")`, `sbx.closePort("tcp", port)`, `inbound(InboundPort.tcp(port, from…))` | Its own public IPv6 address, where it has one (null otherwise); a port opened at it to the sources listed (IPv6 prefixes or addresses, `"::/0"` for everybody), with where it is reached; the ports open; one closed (it says whether it was open); the list, of `CreateOptions` and `UpdateOptions`. Inbound is closed by default; what is open stays open while it sleeps and when it moves. See [networking](/docs/platform/networking/#a-sandboxs-own-ipv6-address). |
| `sbx.revokeToken(token)` | That token alone is refused from then on, and every other works as before: a `Token` (whose `id()`, `tok_…`, says which), the token itself, or its id. A sandbox keeps 1,000 such revocations that have not run out (409 `too_many_revoked_tokens` past them: `revokeTokens()` then). Returns a `TokenRevocation` with its `tokenId()` and `expiresAt()`. |
| `sbx.revokeTokens()` | Every token handed out for this sandbox so far stops working (401 `invalid_token`), those in preview links and cookies and those SSH sessions hold too; tokens made after it work. Needs the API key. Returns a `TokenRevocation`: `sandboxId()`, `generation()`, `revokedAt()`. |
| `sbx.update`, `sbx.sleep()`, `sbx.restart()`, `sbx.delete()`, `sbx.snapshot()`, `sbx.fork(n)`, `sbx.restore(…)` | Change it; sleep; boot from disk; delete; snapshot; fork; restore. |
| `new CreateOptions().checkpoint(10)`, `new UpdateOptions().checkpoint(10)`, `.noCheckpoint()`; `info().checkpoint()` | [Checkpoints](/docs/sandbox/sleep/#checkpoints) while it is awake (1 to 600 seconds apart), so that it goes on with its processes when its machine is lost, and none again; how they go. 409 `checkpoints_unavailable` where no machine takes them. |
| `info().diskWrites()` | What its disk has written today (a UTC day), against the day's share at full speed (a `SandboxInfo.DiskWrites`): `todayBytes()`, of which `checkpointCopyBytes()` its checkpoints' copies wrote to the host's disk; `fullSpeedUntilBytes()`, the share (100 GiB unless its node is told otherwise; null: its node slows nothing, and counts all the same); `limited()` and `limitedUntil()`: past the share its disk takes writes at 2 MiB a second until the day ends, and no write fails. Null on a node that does not copy its disks to an object store, which counts nothing. What it writes to its Drives is not counted here: a Drive's writes are billed by the gigabyte. |
| `glac.sandboxes().snapshots()`: `get`, `list`; `snap.rename(name)`, `snap.expireAt(at)` | Snapshots: memory, processes and disk as they were at an instant; another name for one (null: none), or when it deletes itself (null: never). |
| `glac.sandboxes().sshKeys()`: `create(publicKey, SshKeyOptions)`, `get`, `list` | Public keys that open sandboxes through the SSH gateway; a private key is refused before anything is sent. |

**The shorthand.** `Sandbox.runOnce`, `Sandbox.create`, `Sandbox.get`, `Sandbox.getOrCreate`, `Sandbox.list` and `Sandbox.listPage` are the calls of `Glac.shared().sandboxes()`, one implementation. No other product has statics without a client. The one-off run is `run` on the namespace and `runOnce` on the class, since Java cannot have a static and an instance method with the same signature (`sbx.run` runs a command in a sandbox), and waiting for a command is `waitFor` (`wait()` belongs to `Object`). A command given as a `String` runs with `sh -c`; as a `List<String>` it is a program and its arguments.

```java
Command cmd = sbx.exec("node server.js"); // do not wait
try (Output out = cmd.output()) {
    for (Piece piece : out) {
        System.out.print(piece.text());
    }
}
```

## Drive

A Drive is a disk of the project's that outlives its sandboxes: mounted read-write by one sandbox at a time, when the sandbox is made or while it runs, and kept, with what it holds, when the sandbox goes, for the next sandbox to mount: of its region, or of another whose machines read its home's object store (below). It is copied into its region's object store as it is written, so a sandbox on another node mounts it too: it is brought there first. Only what is written is billed (`usedBytes`). Its snapshots are versions of it in that store, and nothing is copied for them: a fork is a new Drive made of one, a restore takes the Drive back to one, and any number of sandboxes mount one read-only at once (below). Drives are a control plane's (`/v1/drives`): against a node that stands alone every call throws the node's 404 `no_such_route`, and its disks are volumes (below).

```java
Drive data = glac.drives().create(new DriveOptions().name(name).sizeBytes(1L << 30));
Sandbox with = glac.sandboxes().create(new CreateOptions().drive(new DriveMount("/data", data.id())));
with.run("echo kept > /data/note");
glac.drives().detach(with.id(), data.id());
Sandbox other = glac.sandboxes().create();
glac.drives().attach(other.id(), new DriveMount("/data", data.id()));
System.out.println(other.run("cat /data/note").stdout()); // kept
```

| Call | What it does |
|---|---|
| `glac.drives().create(new DriveOptions().name(…).sizeBytes(…).region(…).durability(…))`, `get(idOrName)`, `list()`, `listPage(ListOptions)` | Makes a Drive, empty but for its file system; every option may be left out (`sizeBytes`: a whole number of 4 KiB blocks, at least 16 MiB; 64 GiB unless given, or 5 GiB on Free; `region`: `na`, `eu` or `ap`; `durability`: `standard`, or `strict`, whose guest's fsync returns once the object store has every write before it). Finds one; lists them, newest first; one page. |
| `glac.drives().rename(drive, name)`, `glac.drives().delete(drive)` | Renames it (`null`: no name); deletes it, with what it holds. |
| `glac.drives().attach(sandbox, new DriveMount(path, drive))`, `detach(sandbox, drive)`, `forceDetach(sandbox, drive)` | Mounts it in a sandbox that exists, read-write, at once (a sandbox that sleeps is woken); unmounts it (`forceDetach` while a process uses it). Each returns the Drive. |
| `drive.attach(sandbox, path)`, `drive.detach(sandbox)`, `drive.rename(name)`, `drive.refresh()`, `drive.delete()` | The same, on a `Drive`, whose `info()` is a `Drive.Info`: `id` (a `vol-…`: a Drive is a volume), `name`, `sizeBytes`, `usedBytes`, `attachedTo` (`Drive.Attachment`: the sandbox, the path, the mode), `region`, `home`, `status`, `lease` (`Drive.Lease`: the node of its one writer, the epoch, until when), `appId` and `key` (a keyed instance's), `parentDriveId` and `rootDriveId` (a fork's: the Drive it was forked from, which may be deleted since, and the first of its line; null for a Drive that is no fork), `managedBy` (a `ManagedBy`, the `id` of the database whose Drive it is; null for one that is none's), `createdAt`. A database's Drive is mounted read-write by the database's instance alone, and renamed, restored and deleted through the database: those calls here are refused (409 `managed_by_database`); its snapshots and forks are the project's to take. An app a database is made of says so the same way (`app.info().managedBy()`), and its update, deploy, rollback and delete are refused alike. |
| `glac.drives().snapshot(drive)`, `snapshot(drive, new DriveSnapshotOptions().name(…).fsConsistent())`, `drive.snapshot()` | Takes a snapshot of a Drive, a `DriveSnapshot`: a version of it in the object store, kept; nothing is copied. A Drive a running sandbox has read-write is taken with its guest's writes held for a moment, ten seconds at the most (past them, 503 `unavailable`, retryable): `fsConsistent()` has the sandbox's agent freeze its file system (`consistency` `frozen`; 409 `not_frozen` where the agent cannot: take it without), and without it the machine is paused (`paused`, as at a power cut); one no running sandbox has is `at_rest`. A Drive whose node copies nothing to an object store has none (409 `not_copied`). |
| `glac.drives().snapshots(drive)`, `allSnapshots(driveDeleted)`, `getSnapshot(drive, snapshot)`, `deleteSnapshot(drive, snapshot)`, `drive.snapshots()` | A Drive's snapshots, newest first; the project's of every Drive (`true`: those of Drives deleted since alone; null: all); one, by its id (404 `snapshot_not_found`); deletes one. |
| `glac.drives().fork(drive, new ForkOptions().name(…).fromSnapshot(snapshot))`, `drive.fork(…)`, `snap.fork(name)` | A new Drive whose first version is the snapshot's, or the Drive's as it is now; nothing is copied, and it is mounted at once. Its `parentDriveId` and `rootDriveId` say what it was made of; it is held to the plan's Drives as a create is (403 `drive_limit_reached`). |
| `glac.drives().restore(drive, snapshot)`, `drive.restore(snapshot)`, `snap.restore()` | A new version after the Drive's newest whose content is the snapshot's; the versions in between stay where a snapshot or a fork keeps them, so restoring to an older snapshot and then to a newer one works. A Drive a sandbox has read-write is refused (409 `drive_attached`): detach it first. |
| `new DriveOptions().historySeconds(n)`, `glac.drives().setHistory(drive, n)`, `versions(drive)`, `versionAt(drive, t)`, `new ForkOptions().at(t)`, `restoreTo(drive, t)`; `drive.setHistory`, `drive.versions()`, `drive.restoreTo(t)` | A Drive's window of versions (slice DB8): how far back it can be restored, or forked, by a time, 3600 to 86400 seconds (none unless given; turned off, what it kept goes). Every 30 seconds in which it was written the window gets a version, the Drive at one moment; what they keep alone is billed as a snapshot's is. A restore or a fork to a time takes the newest version at or before it, which the versions call with that time names. `versions` gives `Drive.WindowVersion`s, the newest first; `setHistory(drive, null)` turns the window off; `Drive.Info`'s `historySeconds`, `history`. |
| `glac.drives().attachSnapshot(sandbox, new DriveMount(path, drive).withSnapshot(snapshot))`, `detachSnapshot(sandbox, snapshot)`, `forceDetachSnapshot(sandbox, snapshot)`, `snap.attach(sandbox, path)`, `snap.detach(sandbox)` | Mounts a snapshot read-only in a sandbox that exists; takes it away by the snapshot's id. Each returns the snapshot. When a sandbox is made: `new CreateOptions().drive(new DriveMount(path, drive).withSnapshot(snapshot))`, or `new DriveMount(path, snap)`. |
| `snap.info()`, `snap.refresh()`, `snap.delete()` | A `DriveSnapshot.Info`: `id` (a `dsnap-…`), `driveId`, `name`, `consistency`, `usedBytes` (what the Drive held then), `exclusiveBytes` (what it keeps alone), `exclusiveAt`, `version`, `createdAt`. |

- A Drive is one sandbox's at a time: one that another sandbox has is refused (409 `drive_attached`), and so is deleting one that is attached. A keyed instance's Drive goes with its key (409 `key_drive`).
- One that another node holds is brought to the sandbox's node first; while the object store does not hold it as it is yet (503 `copy_behind`), or it could not be brought (503 `drive_not_brought`), the call is retryable: the client tries again as its `retries` say, then throws, for you to ask again a moment later.
- **Across regions** (slice D9). A Drive's home (`home`) is the region whose object store holds its copy: the region it was made in. A sandbox of another region mounts it too, where that region's machines read its home's store (409 `not_on_one_machine` where they do not): the Drive is let go as a version and taken over there, read from its home's store as its guest reads it, and what its guest writes goes to the store of the region it is in now (`region`). Its home follows it once it has been there for an hour, or at once with `glac.drives().moveHome(drive)` (`drive.moveHome()`): its machine copies what is left in its home's store into its own, and `home` is the new region once nothing is left there; `Drive.Info#homeMove` says how far the move is (`asked_at`, `asked_by`, `copied_objects`, `copied_bytes`, `objects_left`, `error`). Its old home's copy goes once no version kept needs it (a day after). A Drive's home moves once in six hours at the most (409 `home_moved_recently`); one whose home is the region it is in is 409 `home_here`.
- The plan's limits: 3 Drives of at most 5 GiB, 15 GiB together, on Free; 500 of at most 1 TiB on a paid plan (403 `drive_limit_reached`).
- `new UsageOptions().drive(id)`, `kind("drive")` and `groupBy("drive")` report what Drives kept, were written and read cold. The events `drive.created`, `drive.deleted`, `drive.attached` and `drive.detached` carry an `Event.Drive` or an `Event.DriveAttached`.
- **A snapshot outlives its Drive:** it is listed, read, forked and mounted read-only after the Drive is deleted, by the Drive's id. It is billed for what it keeps alone (`exclusiveBytes`), by the hour, at the Drives' data price ($0.02 a GB-month).
- **A fork is billed by its `usedBytes`, its full logical contents, like any Drive: a fork of a 10 GB snapshot is billed as a 10 GB Drive.**
- **Read-only mounts.** Any number of sandboxes mount a snapshot at once, on any machine of the Drive's region, whatever has the Drive and after it is deleted. Its blocks come from the object store as the guest reads them: **those cold reads cost nothing, and show under that sandbox in usage** (`driveColdReadBytes()` of `glac.usageReport(new UsageOptions().sandbox(id))`); reads of what that sandbox's machine already holds cost nothing. The guest cannot write it, and a sandbox that has one does not move to another machine. `attach` takes no mount with a snapshot, `attachSnapshot` none without, and `detachSnapshot` no id but a `dsnap-…`: each is refused before anything is sent.

`glac.volumes()` is the API's `/v1/volumes`, the disks underneath Drives, kept for the transition: what only it has is a node that stands alone, whose disks are volumes, and shared volumes, file systems several sandboxes mount at once. A control plane makes no plain volume for a project's key. It has `create()`, `create(VolumeOptions)` (`name`, `sizeBytes`, `shared`), `get(idOrName)` and `list()`; `vol.delete()` deletes one no sandbox has (one that a sandbox has is 409 `volume_in_use`). A sandbox mounts one with `new CreateOptions().volume(new VolumeMount("/team", vol.id()))`, an app's instances with `AppOptions.volumes(…)`.

**Backups of volumes.** A backup is a copy of a volume (a Drive is one) at one instant, in the object store, kept until it expires: taken by a call, every day by the volume's rule, or before a service's new version replaces its instance. Unlike a snapshot of a Drive it is a copy of its own, a volume is made from it alone, and it outlives its volume; a volume's backups keep only what changed since the one before, and a block several of them need is billed once, on the earliest. It is `making` until the store has all of it, then `ready`, and billed from then by what the store holds of it (`storedBytes`) at the price of what sleeping sandboxes keep ([backups](/docs/drive/volumes/#backups)). Backups are a control plane's.

| Call | What it does |
|---|---|
| `glac.volumes().backup(idOrName)`, `backup(idOrName, new BackupOptions().expiresInDays(7))`, `vol.backup()`, `glac.volumes().backups(idOrName)` | Takes one now, a `Backup`, kept so many days (1 to 365) or as the volume's rule says (30 days unless it was set); a volume's backups, newest first (a deleted volume's by its id). A node that copies nothing to an object store has none (409 `not_copied`). |
| `glac.backups().list()`, `listOf(volumeId)`, `get(id)`, `delete(id)`, `restore(id, name)` | The project's backups, newest first, those of deleted volumes among them; one volume's; one; deletes one (nothing more is billed for it; a volume made from it stays); a new volume made from a ready one (`name` may be null), the same as `glac.volumes().create(new VolumeOptions().fromBackup(id))` (409 `backup_not_ready` while it is being made, 409 `backup_unusable` once it failed). |
| `b.refresh()`, `b.restore(name)`, `b.delete()` | The same, on a `Backup`, whose `info()` is a `Backup.Info`: `id` (`bak-…`), `volumeId`, `volumeName` (the volume's name then), `status` (`making`, `ready`, `failed`), `reason` (`manual`, `daily`, `before_version`), `consistency` (`frozen`, `crash`), `frozenMs` (how long the volume's writers waited for the copy; null where it was not frozen), `sizeBytes` (what the volume held), `volumeSizeBytes` (the most it held: what a volume made from it holds), `storedBytes` (what it is billed for), `ownBytes` (what deleting it would save), `sharedBytes`, `shared`, `region`, `failure`, `createdAt`, `readyAt` (from when it is billed), `expiresAt`. |

- A volume made from a backup has the size and kind of the volume it was taken of, in the backup's region. Its `info().status()` is `restoring` until it holds the backup whole, then `ready`, and nothing attaches it before; `fromBackup()` names the backup. Where the machine it was placed on cannot bring it back, the control plane makes it once more on another machine of the region: `restoreAttempt()` is 2 then, and `firstFailure()` says why the first try failed. If that fails too, it is `failed` (`failure()`), and can only be deleted.
- `new UsageOptions().backup(id)`, `kind("backup")` and `groupBy("backup")` report what backups kept, on the meter of state (`state_gb_hours`); `meters().state().backupsGbHours()` of `glac.billing()` says how much of that meter was backups.

## Domains

A domain of the project's is a name that leads to an app, as its default address does, or to a port of a sandbox. Domains are a control plane's.

| Call | What it does |
|---|---|
| `glac.domains().create(domain, new DomainOptions().app(…))` or `.sandbox(…).port(…)`, with `.proxied(…)`; `get`, `list()`, `list(new ListOptions().app(…))`, `delete`; `domain.update(…)`, `domain.refresh()`, `domain.delete()` | A name for an app or a sandbox's port, `pending_verification` until DNS shows the record its `verification` asks for ([your own domain](/docs/cube/addresses/#your-own-domain)); 409 `domain_exists` when the project has it. |

## Browser

**Not open yet.** Browser is not open to accounts yet: what follows is the SDK as it stands, ahead of its opening.

A session is a hosted Chromium that a client of the Chrome DevTools Protocol (CDP) drives from outside: Playwright for Java connects with `playwright.chromium().connectOverCDP(session.cdpUrl())`. It is awake while a client is connected and falls asleep, its pages kept, a few seconds after the last one leaves. The addresses carry their credential, so `toString()` leaves them out.

- `glac.browsers().create(new BrowserOptions().name("agent-42"))` (or `create()`) makes one, awake; `BrowserOptions` has `kind` (`chromium-headless`, the default, `chromium`, on a screen, or [`lightpanda`](/docs/browser/#lightpanda), a browser without a layout engine), `name`, `region`, `sleepAfterSeconds`, `connectionIdleSeconds`, `deleteAfterIdleSeconds`, `metadata`, `profile(name, save)` and `profile(name, version, save)` (a profile of the project's loaded before the session is answered, saved again when it is deleted where `save`), `proxy(url)` (your own HTTP proxy, which takes `CONNECT`, on a paid plan: the session's connections to ports 80 and 443 go out through it by a relay beside the browser, the credentials kept by the platform; 403 `proxy_not_on_plan` on the free plan). The name of one there is answers that one, woken, with its pages. One whose sandbox fails before it runs is no session: its create is refused, 503 `session_failed`.
- `glac.browsers().get(idOrName)`, `delete(idOrName)` (one made with a profile to save saves it first) and `deleteWithoutSavingProfile(idOrName)`, and a session's own `delete()` and `deleteWithoutSavingProfile()`. A `BrowserSession` has `id()`, `status()`, `cdpUrl()`, `profile()` (a `BrowserSession.Profile`: `name`, `save`, `version`), `proxy()` (a `BrowserSession.Proxy`: its own proxy's `url` and `username`, never the password), `proxyBytes()` (what went through it, both ways) and `info()`: its kind, `liveUrl`, times, `awakeSeconds`, `proxySentBytes` and `proxyReceivedBytes`.
- `status()` is `awake`, `asleep`, `unavailable` (its node cannot be reached) or `failed` (its sandbox failed after its create was answered: its browser stopped and did not come back, say), and then `error()` says why (a `BrowserSession.Failure`: `code`, `message`, as its node says it; `null` unless it failed). The event `browser.failed` says so too: `Event.BrowserFailed.of(event)` reads its data.

A session is billed by the Cube resources it uses, as any instance is: its CPU while it computes, its memory while it is awake and its traffic out ([pricing and plans](/docs/platform/pricing/)). `session.info().awakeSeconds()` is how long it has been awake.

Listing, changing, putting to sleep and new addresses are the Python and TypeScript SDKs' and `glac browsers`'. Sessions are a control plane's.

## Databases

A database is a managed Postgres: a PostgreSQL of its own on a Drive, asleep when nothing asks anything of it and woken by a connection, its sessions kept. The owner's password is in the answer of `create` and of `resetPassword` alone, and kept nowhere: `connectionString()` of the `Database` one of those returned has it.

- `glac.databases().create("shop")`, or `create("shop", new DatabaseOptions().version("18"))`, makes one and waits for its first start (`status()` `ready`, or `creating`: `get` it until it is ready); `DatabaseOptions` has `version`, `region`, `databaseName`, `size(vcpus, memoryBytes)`, `storage(sizeBytes, durability)`, `sleepAfterSeconds`, `suspendAfterSeconds` (paused, a connection answered in a millisecond or two, before its memory is saved and it sleeps for real: 1 to 86400, 300 by default), `wakeDailyAt("03:30")` (woken every day then while it rests), `metadata`. The name of one there is answers that one, without its password. `glac.databases().setSuspendAfter(idOrName, n)` and `setWakeDailyAt(idOrName, "04:00")` change these two, every branch's at once; `null` takes either back (to 300, and to none). On a Free plan a database's rest is fixed, as Neon's Free scales to zero: paused a second after its last query (`sleep_after_seconds` 1) and asleep five minutes later (`suspend_after_seconds` 300); another value is refused 400 `invalid_field`, on a create and on a change.
- `glac.databases().createBranch(idOrName, "preview", parent, expiresAt)` makes a branch of its own (`parent` `null`: `main`; `expiresAt` `null`: it stays): its parent as it is now, nothing copied, with a PostgreSQL, an endpoint and a Drive of its own, at `<database>-<name>` on the project's network; its roles and passwords are its parent's (`branch.connectionString(ownerPassword)`). `deleteBranch(idOrName, branch)` stops its PostgreSQL and deletes its Drive; not `main` (409 `root_branch`), nor a branch others were made of (409 `has_branches`).
- `new DatabaseOptions().historySeconds(n)`, `glac.databases().setHistory(idOrName, n)`, `restore(idOrName, t)`, `restore(idOrName, t, exact)` and `createBranch(idOrName, name, parent, expiresAt, at)`, `createBranch(…, at, exact)` (`Database`'s `historySeconds`, `history`): its history (slices DB8 and DB5): how far back its root branch can be restored, or branched from, by a time: a day unless given; 0 (none) to 30 days on a paid plan, a day on Free. A restore takes the root branch back in place: to a whole minute or a version's own moment, the newest version of its Drive's window at or before it (one every 30 seconds in which it was written, a day of it at the most); further back, to the second, or with `exact`, the moment itself, to the millisecond, by its WAL archive (its WAL replayed to it: seconds to minutes, the call waits). Its endpoint, its connection strings and its owner's password stay what they were, its connections drop, and its state before is kept as a branch of its own, `main_old_<the restore's moment>`; one whose new version does not serve within five minutes is undone (503 `database_restore_failed`). A branch of a time is made of that moment instead (`from: {at, exact}`). A time its history does not have is 400 `invalid_history`, its message saying from when the window and the archive reach. A time to the second (or `exact`) is the archive's or refused, never the window's version before it: 400 `invalid_history` where the archive does not reach it (a database that keeps none, or a time before its oldest base), 503 `database_archive_unavailable` (retryable) while the archive's store refuses its segments; each refusal names the window's version at or before the time, whose own moment can be asked for instead. One whose blocks the store does not give is refused 503 `drive_unreadable` (retryable), nothing made. An outage of the object store long enough that more of its WAL waited than its Drive keeps for the log gives that WAL up, so that the database stays up: a gap of its archive, in `history().gaps()` (`Database.Gap`: `from`, `to`, `fromLsn`, `toLsn`): when the WAL given up began, until when the archive restores no time (the end of the first base taken after it; null until there is one), and where in the log it begins and ends. A time in a gap is restored only from its Drive's window (a whole minute, or a version's own moment); to the second, or exact, it is 400 `invalid_history`. The gap is said as events too: `database.history_gap` when it is first found (once a gap, while the database is awake), and `database.history_resumed` when the archive restores again after it; `Event.DatabaseGap.of(event)` reads their data (null for an event of another type): the database, the gap's `from()`, `to()` (null of the first), `fromLsn()` and `toLsn()`, `segments()` (how many segments of 16 MB were given up) and, of the first, `error()` (what the object store's last refusal said).
- `new DatabaseOptions().tiering(true)` and `glac.databases().setTiering(idOrName, on)` turn its tiered storage on or off (slice DB18: a paid plan, where the object gateway serves; off is refused while a branch has partitions tiered, 409 `tiered_data_exists`; each branch's PostgreSQL restarts once for it); `Database`'s `tiering` says whether it is on and what it bills by now.
- `get(idOrName)`, `list()`, `listNamed(name)`, `delete(idOrName, finalSnapshot)` (a snapshot of its Drive kept seven days unless `finalSnapshot` is false), `branches(idOrName)` (`main` first, then its own), `branch(idOrName, "main")`, `sleep` and `wake` (a branch, now), `resetPassword(idOrName, role)` (a new password in this answer alone; `null` is the database's owner).
- `glac.databases().roles(idOrName)` lists the roles of its root branch that log in, read from PostgreSQL itself (`DatabaseRole`: `name`, `owner`): its owner and the roles you made (`CREATE ROLE … LOGIN`), the platform's not among them. `resetRolePassword(idOrName, role)` gives any of them a new password, in this answer alone: a `DatabaseRoleSecret` with the role's `password` and the root branch's connection strings of that role (`connectionString()`); the owner's is made of the database the API answers. A branch of its own keeps the password it was made with.
- `db.connectionString()` and `branch.connectionString(password)`: the URI libpq and psql take, the public one where the API gives one, else the one on the project's network, with the owner's password (percent-encoded). `Database` and `DatabaseBranch` are records of the API's fields: `defaultBranch()`, `connectionUri()`, `pooledUri()`, `privateUri()`, `endpoint()`, `state()`.

A database is reached on the project's network, from its sandboxes (made with `project_network`) and its services, at its name: that is its `private_uri`. Where an edge serves PostgreSQL it is reached from anywhere at its public address too (`connection_uri`, port 5432, TLS alone; the pooler's: `pooled_uri`); they are null where none does, or where the database has no region to be named under, and the connection string takes them where they are given. `toString()` shows neither the password nor a connection string. Databases are a control plane's.

## Managed Applications

An application of the catalogue ([Rivet](/docs/managed-applications/rivet/), say) is installed in the project as its own Cube resources: its apps, Volumes, secrets and buckets, labelled with the installation, changed through it alone, and billed as they are ([Managed Applications](/docs/managed-applications/)). An install's answer has every output, the secret ones too (an admin token, an endpoint with it in), and a read shows those to a key with the `admin` scope alone: an installation object keeps each output an earlier answer gave that a later one hides.

```java
Catalogue.App rivet = glac.catalogue().get("rivet");      // its parameters, and a month of it in Cube's meters
Installation engine = glac.installations().create("rivet", new InstallationOptions().parameters(Map.of("disk_gib", 20)));
System.out.println(rivet.month() + " " + engine.info().outputs().get("endpoint"));   // RIVET_ENDPOINT, its token in it
```

| Call | What it does |
|---|---|
| `glac.catalogue().list()`, `glac.catalogue().get(app)`, `get(app, version)` | The catalogue's applications, each a `Catalogue.App` (`app`, `version`, `title`, `summary`, `verified`, `licence`, `versions`, `parameters`: a JSON Schema, `outputs`, `month`, and the entry whole in `raw`); one, at its newest version or the one named, with its definition in `raw`. |
| `glac.installations().create(app)`, `create(app, new InstallationOptions().name(..).version(..).region(..).parameters(..).requires(name, installation).noWait())` | Installs it, and waits until it is ready (`noWait`: at once). One that failed throws a `GlacException` of type and code `installation_failed`, with its words. |
| `glac.installations().list()`, `list(app)`, `get(idOrName)` | The project's installations, the newest first; one. |
| `installation.update(new InstallationUpdate().version(..).parameters(..))`, `delete()`, `refresh()`, `await()`, `await(Duration)` | Another version, or its parameters as a merge patch (null takes one back to its default); deleted with everything it is made of (`status()`: `deleted`, or `deleting`); `await(Duration)` ends with `wait_timeout` and the installation goes on. |
| `installation.id()`, `name()`, `status()`, `info()` | As the API says them: `info()` has its `outputs`, `secretOutputs`, `parameters`, `apps`, `volumes`, `buckets`, `requires`, `failure`. |

Managed Applications are a control plane's.

## Secrets and variables

- **Secrets.** `glac.secrets().put("DB_URL", url, List.of("db.example.com"))` keeps a value under a name, with the hosts it may be sent to; `update(name, hosts)` changes only the hosts, `list()` gives the names (never a value), `delete(name)` deletes one. An app's environment takes one by its name (`new AppOptions().secret("VAR", "DB_URL")`), as do an image that is not public (`imagePullSecret`) and a sandbox's egress rules. A call that would send one elsewhere is refused, 403 `secret_host_not_allowed`.
- **Shared variables.** `glac.variables().put(name, value)`, `list()`, `delete(name)`: a value the services' `env` refers to (`"${{ shared.REGION }}"`) in the versions made after it is put. A control plane's.

## The project, the account and the bill

The platform's calls are the client's own: `glac.project()` and `glac.events(EventsOptions)` (the project and a page of its events), `glac.account()` and `glac.updateAccount(AccountOptions)` (its month of traffic, its egress settings, its address, the language of its notices, its billing address and tax id), `glac.usage(ListOptions)` and `glac.usageReport(UsageOptions)` (what was used), `glac.billing()` and `glac.updateBilling(BillingOptions)` (the plan, its meters and credit, the subscription), `glac.balance()`, `glac.updateBalance(BalanceOptions)`, `glac.topUp(usd)` and `glac.topUp(usd, TopUpOptions)` (Credits, auto reload, top-ups), `glac.charges()` and `glac.ledger()`. The bill and the project are a control plane's; a node that stands alone answers them with its own 404.

**The data of an event.** Every type of event the control plane has has a record of its data inside `Event`, named after the description's schema of it, whose `of(event)` reads an event of its types and is null of another's (`Event.Database.of(e)`). A database's: `Event.Database` (`database.created`, with its `region` and PostgreSQL `version`; `database.deleted`, with `final_snapshot_id`, the snapshot of its Drive kept seven days), `Event.DatabaseBranch` (`database.branch_created`, with its `parent`; `database.branch_deleted`, with its `reason`), `Event.DatabaseRestored` (`database.restored`: the moment `at`, and `old_branch_id`, the branch the state before is kept as), `Event.DatabaseArchive` (`database.archive_failing`: `since`, how many segments in a row `failures`, `archived_until`, how far a restore reaches meanwhile, and the object store's `error`; `database.archive_recovered`: `recovered_at`) and `Event.DatabaseGap`. The others likewise: a sandbox's (`Event.SandboxSlept`, `Event.SandboxWoke`, `Event.SandboxMoved`, `Event.SandboxDeleted`, …), an app's and its keys' (`Event.App`, `Event.AppFailed`, `Event.KeyCreated`, …), the project's deploys and key, backups, domains, destinations and Drives' snapshots (`Event.ProjectDeployed`, `Event.Backup`, `Event.Domain`, `Event.Destination`, `Event.DriveSnapshot`, …), and the account's plan, top-ups, refunds, withdrawals, balance, Credits, storage, egress cap, spend limits, hold and debts (`Event.SubscriptionPeriod`, `Event.TopupCollected`, `Event.WithdrawalReceived`, `Event.Balance`, `Event.InDebt`, …). A field that not every event of a type has, or that is null, is null (a number of them a `Long` or a `Double`).

**Webhooks, API keys, spend limits, the project.** `glac.webhooks().create(url, new WebhookOptions()…)` sends the project's events to an address of yours (the `secret()` that signs each delivery is in that answer alone); `get`, `list`, `update`, `delete`. `glac.keys().create(name, scopes, expiresAt)` makes an API key (the key itself, `key()`, is in that answer alone); `get`, `list`, `delete`. `glac.limits()` and `glac.updateLimits(50.0)` are the project's spend limit and what was spent this month (`null` takes the limit away), and `sbx.limits()`, `sbx.updateLimits(…)` a sandbox's own. `glac.requestLimit(limit, wanted, why)` asks for one of the account's [limits raised on request](/docs/platform/raising-limits/) (a cap by its name, `"volumes"` or `"volume_bytes"`; bytes of the two in bytes; 409 `limit_request_pending` while one of the same limit waits), and `glac.limitRequests(new ListOptions())` and `glac.limitRequest(id)` read them with the operator's answers (a `LimitRequest`: `status` `pending`, `granted` or `refused`, `granted`, `answer`). `Event.InstanceRestarted.of(event)`, `Event.InstanceUpgrade.of` (`instance.upgraded`, `.reverted`, `.upgrade_failed`) and `Event.LimitRequestAnswer.of` read those events' data; a sandbox's (and an instance's) `info().startCommand()` says how its start command runs, how often it was started again in its machine, and how it last ended. `glac.updateProject(30)` sets after how many days asleep its sandboxes are deleted (`null`: never). A control plane's. Webhooks and API keys need a key with the `admin` scope, and so do setting the project's limit and `updateProject`; setting a sandbox's limit needs `write`.

**Log destinations, the audit log, the billing documents.** `glac.destinations()` is where the logs (and the metrics and the requests' traces: `.signals("logs", "traces")`) of the project's apps are exported (OpenTelemetry endpoints): `create(name, new DestinationOptions().endpoint(u).header(name, value))`, which tries it at once with one record (`status().connected()`), `get`, `list`, `update`, `delete`, `test` (one record now); a header's value is a credential, sent when a destination is made or changed and in no answer (what is returned has the headers' names alone). `glac.audit(new ListOptions().limit(n))` is a page of the project's audit log, the newest first (who, what method, what path, what status): every call that was no reading, a refused one too, and every reading of an app's log by an operator. `glac.statement("2026-09")` is a month's statement (`null` is this month's) with each database's own line in it (`databases()`: what it used in the month, its history and its tiered data apart, and what that comes to at the list prices), `glac.invoices()` the invoices at Stripe (with the PDF's link), `glac.notices()` the mail the account was sent, `glac.withdrawals(new ListOptions())` what it may withdraw from now and the withdrawals it made (a consumer in the EEA or the UK may withdraw from a purchase within 14 days), and `glac.withdraw(chargeId, name, email)` the buyer's own statement of withdrawal, received at once. A control plane's. Making, changing, testing and deleting a destination, reading the audit log and withdrawing need a key with the `admin` scope; the rest, one that may read.

The account's `email()` is where its notices go, which `new AccountOptions().email(address)` sets: `emailVerified()` says whether it is verified (only a verified address is mailed: one that is not gets a link first), and `emailUndeliverable()` (an `Account.EmailUndeliverable`: `why`, `since`, `detail`) that mail to it bounced; meanwhile the notices wait ([where notices go](/docs/platform/projects/#where-notices-go)). The account's `language()` is the language its notices are written in (an `Account.Language`: `code` "en", "de" or "fr", and `source` "account", "billing_address" or "default"); `new AccountOptions().language("de")` chooses it, and `noLanguage()` takes the choice back, so that the billing address says again. The acknowledgement of a withdrawal and the confirmation of a purchase are written in it; every other notice is English.

The account's `keeps()` (an `Account.Keeps`, behind a control plane; null on a node by itself) is what the key's project keeps of Drives and volumes, by the numbers a create is held to (403 `drive_limit_reached`, `volume_limit_reached`), each with what the project has: `volumes` (an `Account.KeepsCount`: `limit`, `used`, and `source`, "plan" or "operator"), `volumeBytes` (an `Account.KeepsSize`: how large one may be, and its `source`), `volumesBytes` (an `Account.KeepsTotal`: how large all of them may be together, `limit` null for no limit, and `used`) and `unsizedBytes`, the size of one made with no size. A number of the account's own ("operator") is [asked for](/docs/platform/raising-limits/) with `glac.requestLimit("volumes", …)` or `"volume_bytes"`.

**Buying at once (`startNow`).** A consumer in the European Economic Area or the United Kingdom (an account with no tax id that is verified or being verified) may withdraw from a purchase within 14 days, and what it buys is provided at once only at its own express request. `startNow()` carries that request: `glac.topUp(20, new TopUpOptions().startNow())`, `new BillingOptions().plan("p20").startNow()` (a plan that is bought: from Free, or a move up) and `new BalanceOptions().autoReload(true).startNow()` (for every auto reload while auto reload stays on), in the words the console shows beside its box (and `GET /v1/billing/withdrawals` has, `right.start_now`):

> I ask for this purchase to be provided at once, within the 14 days in which I may withdraw from it. If I withdraw, I pay for what was provided until then. I understand that once all the Credits of a top-up are used, it has been provided in full and I can no longer withdraw from it.

The service starts now; within the 14 days the buyer may still withdraw, and pays for what was provided until then; once it is fully provided (a top-up whose Credits are all used), the right to withdraw is lost. Give it only when the person buying asks for that: the SDK never sends it unless it is given. Without it, such an account is refused: a `GlacException` of status 400, code `start_now_required`, `param()` `start_now`, in words that name its country and quote the request, and nothing is bought. An account whose auto reload is on without the request reloads nothing, and is told once a month (the event `auto_reload.no_request`, read with `Event.AutoReloadNoRequest.of`; an account with no card on record, `auto_reload.no_card` and `Event.AutoReloadNoCard`).

## Errors

A non-zero exit code is a result: read `res.exitCode()`. An error of the API throws `GlacException` with `status()`, `type()`, `code()`, `param()`, `retryable()`, `requestId()` and `message()`, and `docsUrl()`, which leads to the code on the [errors](/docs/errors/) page; so does a call that got no answer (`status()` 0, `type()` `unavailable` or `timeout`). It is the platform's error, whichever product the call is of. Errors about money (402 `account_in_debt`, 403 `trust_limit_exceeded`, 409 `no_card`, 400 `start_now_required`, …) carry the control plane's own message, which says what lifts them.

## Behaviour worth knowing

- **Sleeping sandboxes.** No call fails because a sandbox sleeps or is starting: it waits and goes on. `res.wake()` says how long `run` waited for a wake; `onResponse` on the client sees it for every call.
- **Retries.** No answer, or a `retryable` error, is retried twice (`retries`), from 0.2 s doubling. Every `POST` carries an `Idempotency-Key`; `idempotencyKey(key)` lets a program that restarts get back what it made.
- **Timeouts.** `timeout` (15 minutes unless you say) is how long to wait for the API; `RunOptions.timeout` is the command's own limit.
- **Threads.** `Glac`, its namespaces, `Sandbox`, `Command` and the files object may be used from several threads. The SDK starts one daemon thread of its own, which watches event streams.

Source: https://glac.ai/docs/sdks/java/index.mdx
