# Object storage from the API

The [Object storage quick start](/storage/quick-start) shows how to talk to a bucket with an S3 client. This page is the **management** side: creating the bucket and its keys and reading usage, all through the Zaroz API instead of the console. Files themselves still go through the S3 endpoint (or the signed links below). The samples use the helper from [Using the Zaroz API](/guides/api/overview).

Object storage may be absent on some deployments: check `object_storage_buckets` in [what the deployment offers](/guides/api/overview#what-a-deployment-offers). Everything here is addressed by the **subscription** id of your object storage plan, and writing needs the authority to buy for the organization (Owner or Admin).

## Create the bucket

One bucket per subscription. `cap_bytes` is the storage cap (1 GiB to 100 TiB, default 100 GiB); `region_id` defaults to the region of the plan.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"label": "site-assets", "cap_bytes": 107374182400}'
```

```json
{
  "data": {
    "id": "…", "label": "site-assets",
    "endpoint": "https://…", "bucket_name": "zb-3f2a…", "s3_region": "us-east-1",
    "addressing_style": "path",
    "cap_bytes": 107374182400, "stored_bytes": null,
    "public_read": false, "public_url": null, "access": "open", "purge_at": null
  }
}
```

**TypeScript**

```ts
import { call } from "./zaroz";

const b = await call("POST", `/subscriptions/${SUB}/bucket`, { label: "site-assets", cap_bytes: 100 * 1024 ** 3 });
const { endpoint, bucket_name } = b.body.data;
console.log(endpoint, bucket_name); // the two values your S3 client needs
```

**Python**

```python
from zaroz import call

_, b = call("POST", f"/subscriptions/{SUB}/bucket", {"label": "site-assets", "cap_bytes": 100 * 1024**3})
print(b["data"]["endpoint"], b["data"]["bucket_name"])   # the two values your S3 client needs
```

A second bucket answers `409`, and an inactive subscription `422`. `bucket_name` is the real name to use in S3 requests (not your label). `access` says what the data plane allows right now: `open`, `read_only` (over the cap), `suspended` or `deleted`. `GET /subscriptions/{id}/bucket` returns the same object (`404` when there is no bucket yet).

## Change the cap, the label or public read

```bash
curl -sS -X PATCH "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"public_read": true, "cap_bytes": 214748364800}'
```

```ts
await call("PATCH", `/subscriptions/${SUB}/bucket`, { public_read: true, cap_bytes: 200 * 1024 ** 3 });
```

```python
call("PATCH", f"/subscriptions/{SUB}/bucket", {"public_read": True, "cap_bytes": 200 * 1024**3})
```

Absent fields are left alone. A cap change and a public-read change reach the data plane at once. With `public_read` on, anyone can download an object at `public_url/KEY` without a key (listing and writing still need one); turn it off to close it. Not allowed once the bucket is scheduled for deletion (`422`).

## Access keys

A key is `read_only` or `read_write` and works only on this bucket. At most **20** active keys per bucket (`429` past it).

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket/keys" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "ci-upload", "scope": "read_write"}'
# {"data": {"id": "…", "access_key_id": "…", "scope": "read_write", "name": "ci-upload"}, "secret_access_key": "…"}

curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket/keys" -H "$AUTH"
curl -sS -X DELETE "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket/keys/$KEY" -H "$AUTH"
```

**TypeScript**

```ts
const k = await call("POST", `/subscriptions/${SUB}/bucket/keys`, { name: "ci-upload", scope: "read_write" });
console.log(k.body.data.access_key_id, k.body.secret_access_key); // the secret is shown ONCE
await call("DELETE", `/subscriptions/${SUB}/bucket/keys/${k.body.data.id}`); // revoke
```

**Python**

```python
_, k = call("POST", f"/subscriptions/{SUB}/bucket/keys", {"name": "ci-upload", "scope": "read_write"})
print(k["data"]["access_key_id"], k["secret_access_key"])   # the secret is shown ONCE
call("DELETE", f"/subscriptions/{SUB}/bucket/keys/{k['data']['id']}")   # revoke
```

The secret is returned **only in this response**; listing never shows it, and Zaroz cannot show it again. Use `access_key_id` and `secret_access_key` as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in the [quick start](/storage/quick-start#3-connect). Revoking stops that key at once.

## Usage and the bill

```bash
curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket/usage" -H "$AUTH"
curl -sS -G "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket/usage" -H "$AUTH" \
  --data-urlencode "from=2026-10-01T00:00:00Z" --data-urlencode "to=2026-10-15T00:00:00Z"
```

```ts
const u = await call("GET", `/subscriptions/${SUB}/bucket/usage`);
console.log(u.body.data);
```

```python
_, u = call("GET", f"/subscriptions/{SUB}/bucket/usage")
print(u["data"])
```

Without a window you get this month so far (at most 93 days): hourly stored-byte and traffic samples, the average stored size in GiB-months and the bytes sent out, which are the two billed meters. Uploads are free.

## Browse and sign transfers

If you do not want a key at all (for instance a web back end that hands links to browsers), ask the API to sign a link per object. Listing is metadata only and not billed.

**curl**

```bash
O=/subscriptions/$SUBSCRIPTION/bucket/objects
curl -sS -G "$ZAROZ_API$O" -H "$AUTH" --data-urlencode "prefix=photos/" --data-urlencode "limit=100"

curl -sS -X POST "$ZAROZ_API$O/upload" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"key": "photos/a.jpg", "size_bytes": 482344, "content_type": "image/jpeg"}'
# up to 64 MiB: a signed PUT of exactly that many bytes. Larger (up to 625 GiB): a multipart upload.

curl -sS -X POST "$ZAROZ_API$O/download" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"key": "photos/a.jpg", "save_as": "holiday.jpg"}'
# a 15-minute signed GET
```

**TypeScript**

```ts
const O = `/subscriptions/${SUB}/bucket/objects`;
const start = await call("POST", `${O}/upload`, { key: "photos/a.jpg", size_bytes: file.byteLength, content_type: "image/jpeg" });
// For a single PUT upload, send exactly size_bytes bytes to the signed URL in start.body.data

const dl = await call("POST", `${O}/download`, { key: "photos/a.jpg" });
console.log(dl.body.data); // the signed URL, valid for 15 minutes
```

**Python**

```python
O = f"/subscriptions/{SUB}/bucket/objects"
_, start = call("POST", f"{O}/upload", {"key": "photos/a.jpg", "size_bytes": len(data), "content_type": "image/jpeg"})
# For a single PUT upload, send exactly size_bytes bytes to the signed URL in start["data"]

_, dl = call("POST", f"{O}/download", {"key": "photos/a.jpg"})
print(dl["data"])   # the signed URL, valid for 15 minutes
```

Downloads go straight to the storage endpoint and are billed as egress like any S3 client, previews included. Larger uploads are multipart: `POST .../objects/upload/parts` signs each part, `.../upload/complete` finishes and `.../upload/abort` cancels. `POST .../objects/delete` deletes objects and `POST .../objects/folders` creates a folder marker. Uploads are refused while the bucket is over its cap or suspended. For heavy or repeated use, an access key and an S3 client are simpler than signing each object.

## Delete the bucket

```bash
curl -sS -X DELETE "$ZAROZ_API/subscriptions/$SUBSCRIPTION/bucket" -H "$AUTH"      # 204
```

```ts
await call("DELETE", `/subscriptions/${SUB}/bucket`);
```

```python
call("DELETE", f"/subscriptions/{SUB}/bucket")
```

The keys stop working immediately and the slot frees, but the data stays for a **seven-day recovery window** (`purge_at` shows the date) before it is purged for good.
