# Buckets

> S3-compatible object storage: create buckets, presign object URLs, list and delete objects, set CORS, attach to apps.

A bucket holds objects and speaks the S3 protocol. Your app reaches it in two ways: ask this API for a presigned URL per object, or attach the bucket to the app, which gives the app a key scoped to that bucket and the `S3_*` environment variables to use it directly. See the guide [Buckets](https://docs.rytiv.com/guides/buckets.md).

Attaching and detaching a bucket go through the app's `attach` and `detach` routes (see [Apps](https://docs.rytiv.com/reference/api/apps.md)) with a body of `{"bucket": "<name>", "as": "<prefix>"}`.

## Endpoints

- [`POST /v1/buckets`](#post-v1buckets): Creates a bucket. Names are unique across Rytiv
- [`GET /v1/buckets`](#get-v1buckets): The organization's buckets
- [`GET /v1/bucket-names/:name`](#get-v1bucket-namesname): Whether a bucket name is still available, and if not, why
- [`GET /v1/buckets/:name`](#get-v1bucketsname): One bucket
- [`DELETE /v1/buckets/:name`](#delete-v1bucketsname): Deletes a bucket
- [`POST /v1/buckets/:name/presign`](#post-v1bucketsnamepresign): A time-limited URL for one object and one method (`get` or `put`)
- [`GET /v1/buckets/:name/objects`](#get-v1bucketsnameobjects): Lists objects, with a cursor for the next page
- [`DELETE /v1/buckets/:name/objects`](#delete-v1bucketsnameobjects): Deletes objects by key
- [`GET /v1/buckets/:name/cors`](#get-v1bucketsnamecors): The bucket's CORS rules
- [`PUT /v1/buckets/:name/cors`](#put-v1bucketsnamecors): Replaces the bucket's CORS rules. An empty list removes them
- [`GET /v1/bucket-attachments`](#get-v1bucket-attachments): Which bucket is attached to which app, and the environment it injected

## POST /v1/buckets

Creates a bucket. Names are unique across Rytiv.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Request body

JSON, the shape `CreateBucketRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | required | The bucket's name. Unique across Rytiv. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/buckets" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-bucket"}'
```

### Response

`201` with the bucket:

```json
{
  "id": "<id>",
  "name": "my-bucket",
  "orgId": "<orgId>",
  "backendRef": "<backendRef>",
  "createdAt": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The bucket's id. |
| `name` | string | always |  |
| `orgId` | string | always | The organization that owns it. |
| `backendRef` | string | always | Where the objects live, in the storage layer's own words; opaque. |
| `createdAt` | string (ISO 8601) | always | When it was created. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/buckets

The organization's buckets.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/buckets" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a list of buckets, each:

```json
[
  {
    "id": "<id>",
    "name": "my-bucket",
    "orgId": "<orgId>",
    "backendRef": "<backendRef>",
    "createdAt": "2026-10-21T09:00:00Z"
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The bucket's id. |
| `name` | string | always |  |
| `orgId` | string | always | The organization that owns it. |
| `backendRef` | string | always | Where the objects live, in the storage layer's own words; opaque. |
| `createdAt` | string (ISO 8601) | always | When it was created. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |

## GET /v1/bucket-names/:name

Whether a bucket name is still available, and if not, why.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The name to check. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/bucket-names/<name>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the answer:

```json
{
  "name": "my-app",
  "available": true
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `name` | string | always |  |
| `available` | boolean | always | Whether you can have the name. |
| `reason` | `taken` \| `taken-by-you` \| `reserved` \| `invalid` | if set | If not: `taken`, `taken-by-you`, `reserved` or `invalid`. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/buckets/:name

One bucket.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/buckets/<name>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the bucket:

```json
{
  "id": "<id>",
  "name": "my-bucket",
  "orgId": "<orgId>",
  "backendRef": "<backendRef>",
  "createdAt": "2026-10-21T09:00:00Z"
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `id` | string | always | The bucket's id. |
| `name` | string | always |  |
| `orgId` | string | always | The organization that owns it. |
| `backendRef` | string | always | Where the objects live, in the storage layer's own words; opaque. |
| `createdAt` | string (ISO 8601) | always | When it was created. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## DELETE /v1/buckets/:name

Deletes a bucket.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Query parameters

| Name | Description |
| --- | --- |
| `force` |  |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/buckets/<name>" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON object carrying `destroyed`, `deletedObjects`, `detached`.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## POST /v1/buckets/:name/presign

A time-limited URL for one object and one method (`get` or `put`).

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Request body

JSON, the shape `PresignRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `op` | `get` \| `put` | optional | `get` for a download URL, `put` for an upload URL. default `"get"`. |
| `key` | string | required | The object's key. 1 to 1024 characters. |
| `expiresInS` | integer | optional | How long the URL stays valid, in seconds. 1 to 604800, default `900`. |
| `contentType` | string | optional | `put` only: the Content-Type the upload must send. It is part of the signature. 1 to 255 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/buckets/<name>/presign" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key":"uploads/report.pdf"}'
```

### Response

`200` with the presigned URL:

```json
{
  "url": "<url>",
  "method": "GET",
  "key": "uploads/report.pdf",
  "expiresAt": "2026-10-21T09:00:00Z",
  "requiredHeaders": {
    "<key>": "<value>"
  }
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `url` | string | always | The URL to call with `method`. |
| `method` | `GET` \| `PUT` | always | With `path`: `GET` (default) or `POST`. |
| `key` | string | always | The object's key. |
| `expiresAt` | string (ISO 8601) | always | When the copy is deleted (a moment). Not together with `ttlSeconds`. |
| `requiredHeaders` | object of string | always | Headers the call must send exactly; a missing one breaks the signature. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/buckets/:name/objects

Lists objects, with a cursor for the next page.

**Auth:** API key with full access or the scope `read`. An agent token of class `production` needs `prodDataRead` for it; `sandbox` and `twin` may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Query parameters

| Name | Description |
| --- | --- |
| `limit` | How many objects per page. |
| `prefix` | Only keys that start with this. |
| `cursor` | The `nextCursor` of the previous page. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/buckets/<name>/objects" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the page of objects:

```json
{
  "bucket": "my-bucket",
  "objects": [
    {
      "key": "uploads/report.pdf",
      "sizeBytes": 0,
      "lastModified": "<lastModified>"
    }
  ]
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `bucket` | string | always |  |
| `objects` | array of object | always |  |
| `nextCursor` | string | if set | Hand it back as `cursor` to get the next page. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## DELETE /v1/buckets/:name/objects

Deletes objects by key.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Query parameters

| Name | Description |
| --- | --- |
| `key` |  |

### Example request

```bash
curl -X DELETE "https://api.rytiv.app/v1/buckets/<name>/objects" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a JSON object carrying `bucket`, `deleted`.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/buckets/:name/cors

The bucket's CORS rules.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/buckets/<name>/cors" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with the rules:

```json
{
  "rules": [
    {
      "allowedMethods": [
        "GET"
      ],
      "allowedOrigins": [
        "<allowedOrigin>"
      ]
    }
  ]
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `rules` | array of object | always | The whole CORS configuration; a `PUT` replaces it. |
| `warnings` | array of string | if set |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## PUT /v1/buckets/:name/cors

Replaces the bucket's CORS rules. An empty list removes them.

**Auth:** API key with full access (a key with scopes cannot call this route). An agent token of class `sandbox` or `twin` may call it; class `production` is refused (`agent-token-production-readonly`).

### Path parameters

| Name | Description |
| --- | --- |
| `name` | The bucket's name. |

### Request body

JSON, the shape `PutBucketCorsRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `rules` | array of object | required | The whole CORS configuration; a `PUT` replaces it. 0 to 100 items. |

### Example request

```bash
curl -X PUT "https://api.rytiv.app/v1/buckets/<name>/cors" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"rules":[{"allowedMethods":["GET"],"allowedOrigins":["<allowedOrigin>"]}]}'
```

### Response

`200` with the rules as stored:

```json
{
  "rules": [
    {
      "allowedMethods": [
        "GET"
      ],
      "allowedOrigins": [
        "<allowedOrigin>"
      ]
    }
  ]
}
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `rules` | array of object | always | The whole CORS configuration; a `PUT` replaces it. |
| `warnings` | array of string | if set |  |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |

## GET /v1/bucket-attachments

Which bucket is attached to which app, and the environment it injected.

**Auth:** API key with full access or the scope `read`. Every class of agent token may call it.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/bucket-attachments" \
  -H "Authorization: Bearer $RYTIV_TOKEN"
```

### Response

`200` with a list of attachments, each:

```json
[
  {
    "appName": "my-app",
    "bucketName": "<bucketName>",
    "injected": [
      "<injected>"
    ]
  }
]
```

| Field | Type | Present | Description |
| --- | --- | --- | --- |
| `appName` | string | always |  |
| `bucketName` | string | always |  |
| `injected` | array of string | always | The environment variables the attachment gave the app. |

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
