The Console developer API covers the endpoints third-party developers use to manage spaces, folders, and files, plus the [Seal](/docs/data-security) endpoints that back private-folder access control.

> **Info**
>
> The Console developer API is in beta at `https://api.console.walrus.xyz`. Endpoint shapes might change before GA. For a guided walkthrough of the full encrypted flow, start with the [Quick Start](./quickstart). For the product model behind these endpoints, see the [concepts and overview](./overview).
## Authentication

Every request carries an API key as a bearer token:

```http
Authorization: Bearer hbr_…
```

Mint keys through the Console web interface. Each key carries one of three scopes:

1. `read_write` keys can change state: create and delete folders, upload and delete files, and finalize private folders.
2. `read_only` keys can list, check status, mint download URLs, and download. Any other write with a read-only key returns `403` with code `read_only_api_key`.
3. `key_admin` keys belong to Management API keys. They reach only the API-key endpoints and the Seal sponsor endpoints, and every data-plane endpoint returns `403`. This page covers the data-plane keys. For programmatic key minting, see the [MCP server](./mcp-server#mint-keys-for-other-agents).

A request with no valid key returns `401`. A key without access to the target resource returns `403`.

## Base URL and conventions

All paths are relative to the base URL and prefixed with `/api/v1`:

```
https://api.console.walrus.xyz
```

Request and response bodies are JSON unless noted. File upload uses `multipart/form-data`, and download redirects to a separate content host. Resource identifiers are UUIDs. Response keys are `snake_case`. Request bodies keep the field names shown in each section, some of which are `camelCase`.

### Pagination

List endpoints return an opaque cursor. Pass `limit` to set page size and `cursor` to fetch the next page. When there are no further results, the cursor field returns `null`. Folder lists return the cursor as `next_cursor`. File lists and search return a `pagination` object with `limit`, `has_more`, and `next_cursor`.

### Asynchronous operations

Console processes upload and delete operations asynchronously. Upload returns `202` with a file ID, then you poll the file status endpoint until the job state is `completed`. Delete returns `204` immediately and releases storage after a background worker confirms the removal. Deleting a folder also deletes every file in it.

## Errors

Most error responses use the shape:

```json
{ "error": "Human-readable message", "code": "machine_readable_code" }
```

The `code` field is present when a machine-readable value applies. Some folder errors, such as a `409` name conflict, carry `error` only. Validation failures on the file and search routes return `400` with code `bad_request`. Validation failures on the other routes return `400` with the raw validator output instead:

```json
{ "success": false, "error": { "name": "ZodError", "issues": [ … ] } }
```

Common error codes include:

| **Status** | **Code** | **Meaning** |
| --- | --- | --- |
| `401` | `unauthorized` | Missing or invalid API key. |
| `401` | `api_key_registering` / `api_key_revoking` / `api_key_revoked` | The key is not currently usable. |
| `403` | `read_only_api_key` | A write was attempted with a read-only key. |
| `403` | `insufficient_scope` | The key's scope does not cover the endpoint. |
| `403` | `bucket_not_in_scope` | The key cannot access the target folder, or the space in the path is not the key's space. |
| `403` | `mirror_missing_grant` | The onchain access grant has not yet mirrored into the access index. Retry. |
| `409` | `bucket_not_finalized` | The folder has not completed the finalize step. |
| `409` | `bucket_deleting` / `bucket_gone` | The folder is being deleted, or its storage is already gone. |
| `410` | `sponsor_digest_expired` | The sponsor digest is unknown, expired, or bound to a different key. |
| `413` | `payload_too_large` | The upload exceeds the 100 MiB per-upload cap. |
| `415` | `unsupported_file_type` | The request content type is not accepted. |
| `422` | `quota_exceeded` | The operation exceeds the storage quota. |
| `422` | `plan_limit_exceeded` | The operation exceeds the folder or storage limit. See the shape below. |
| `423` | `file_deleting` | The file is being deleted. |
| `429` | `TooManyRequestsError` | The rate limit was exceeded. Read `Retry-After`. |
| `400` | `bad_request` | The request failed validation on a file or search route. |

Requests that exceed a limit return `422` with a distinct shape:

```json
{
  "code": "plan_limit_exceeded",
  "limit": "buckets",
  "limit_value": 5,
  "current_tier": "free",
  "upgrade_url": "/account"
}
```

The `limit` value is one of `storage`, `users`, or `buckets`. `limit_value` is present for `buckets` and `users`. Each space allows 5 GB of storage and five folders, and each upload is capped at 100 MiB.

## Spaces

A space is the top-level container for your data. Console creates a Personal Space at sign-up.

### List spaces

`GET /api/v1/spaces`

Lists all spaces accessible to the authenticated user.

| **Parameter** | **Location** | **Required** | **Description** |
| --- | --- | --- | --- |
| `type` | query | No | Filter by space type: `personal` or `team`. |

Returns `200` with a `data` array of spaces. Each space includes `id`, `type`, `name`, `plan`, `storage_used`, `storage_cap`, `bucket_count`, `member_count`, `role`, and `created_at`. `member_count` is `null` for a Personal Space.

### Get storage usage

`GET /api/v1/usage`

Returns the storage usage of the key's space. Returns `200` with `data.storage_used`, `data.storage_cap`, and `data.available` in bytes, `data.percent_used` as a fraction from 0 to 1, and `data.breakdown` with the usage grouped by file category and by file format.

### List folders in a space

`GET /api/v1/spaces/{id}/buckets`

| **Parameter** | **Location** | **Required** | **Description** |
| --- | --- | --- | --- |
| `id` | path | Yes | Space UUID. |
| `limit` | query | No | Page size, 1 to 1000. Default 100. |
| `cursor` | query | No | Cursor from a previous page. |
| `q` | query | No | Case-insensitive substring match on folder name. |
| `visibility` | query | No | Filter by `public` or `private`. |
| `sortField` | query | No | `name`, `date`, or `storage`. Default `date`. |
| `sortOrder` | query | No | `asc` or `desc`. Default `desc`. |

Returns `200` with `buckets` and `next_cursor`. Each folder has the fields listed under [Get a folder](#get-a-folder).

### Create a folder

`POST /api/v1/spaces/{id}/buckets`

Reserves a private folder. This is the first step of the reserve, sign, and finalize handshake. In beta, `scope` accepts `private` only.

Body:

```json
{ "name": "secrets", "scope": "private" }
```

Returns `201` with the reservation:

```json
{
  "bucket_id": "…",
  "bytes": "<base64 sponsored Sui transaction>",
  "digest": "…",
  "provisioning_state": "pending_policy",
  "owner_address": "0x…",
  "admin_signer_address": null
}
```

Sign `bytes` locally with your service key, then call the finalize endpoint. `owner_address` is the Sui address of the Console account that owns the folder. `admin_signer_address` is the address of the space's Management API key signer, or `null` when the space has none. A folder in `pending_policy` is not visible to the file endpoints, which return `404` for it until finalize succeeds. A name conflict returns `409`. Exceeding the folder limit returns `422`. See the [Quick Start](./quickstart) for the full signing flow.

### List files in a space

`GET /api/v1/spaces/{id}/files`

Lists files across every folder in the space.

| **Parameter** | **Location** | **Required** | **Description** |
| --- | --- | --- | --- |
| `id` | path | Yes | Space UUID. |
| `limit` | query | No | Page size, 1 to 100. Default 20. |
| `cursor` | query | No | Cursor from a previous page. |
| `q` | query | No | Case-insensitive substring match on file name. |
| `sortField` | query | No | `name`, `date`, `size`, or `type`. Default `date`. |
| `sortOrder` | query | No | `asc` or `desc`. Default `desc`. |

Returns `200` with `data` and a `pagination` object (`limit`, `has_more`, `next_cursor`).

## Folders

A folder holds files inside a space. Private folders store [Seal](/docs/data-security) ciphertext only.

### Get a folder

`GET /api/v1/buckets/{id}`

Returns `200` with the folder under `data`: `id`, `space_id`, `name`, `oyster_bucket_name`, `walrus_bucket_id`, `visibility`, `status`, `provisioning_state`, `seal_policy_id`, `storage_used`, `file_count`, `metadata`, `last_opened_at`, `expires_at`, `created_at`, and `updated_at`. `status` is one of `active`, `deleting`, or `deleted`. Just after finalize, this endpoint can also return `403` with code `mirror_missing_grant` until the access grant propagates.

### Update a folder

`PUT /api/v1/buckets/{id}`

| **Field** | **Required** | **Description** |
| --- | --- | --- |
| `name` | Yes | New folder name, 1 to 100 characters in length. |
| `sealPolicyId` | No | Seal policy ID. A private folder cannot clear it. |
| `visibility` | No | Immutable in v1. Changing it returns `403`. |

Returns `200` with the updated folder under `data` (`id`, `name`, `visibility`, `updated_at`). A name conflict returns `409`. Unknown fields return `400`.

### Get folder metadata

`GET /api/v1/buckets/{id}/metadata`

Returns `200` with the folder's description, tags, and statistics under `data`: `description`, `tags`, `size`, `asset_count`, `created_at`, `updated_at`, and `last_opened_at`.

### Update folder metadata

`PATCH /api/v1/buckets/{id}/metadata`

| **Field** | **Required** | **Description** |
| --- | --- | --- |
| `description` | No | Free text, up to 1000 characters in length. Pass `null` to clear. |
| `tags` | No | Up to 10 tags, each up to 24 characters in length. |

Send at least one field. Returns `200` with the same shape as the metadata endpoint under `data`.

### Delete a folder

`DELETE /api/v1/buckets/{id}?confirm=true`

Requires the `confirm=true` query parameter. The delete cascades to every file in the folder: each file moves to `deleting`, and a background worker releases the storage afterward. Returns `204` with an `X-Deleted-Id` header. A missing confirmation or a validation error returns `400`.

### Finalize a private folder

`POST /api/v1/buckets/{id}/finalize`

Submits the signature over the reserved transaction bytes to activate a pending private folder.

Body:

```json
{ "signature": "<base64 signature>" }
```

Returns `200`:

```json
{ "bucket_id": "…", "seal_policy_id": "…", "provisioning_state": "active" }
```

`seal_policy_id` is the onchain bucket-policy object ID used by Seal for access checks. You can now upload to the folder.

### List files in a folder

`GET /api/v1/buckets/{id}/files`

Takes the same `limit`, `cursor`, `q`, `sortField`, and `sortOrder` parameters as the space file listing. Returns `200` with `data` and `pagination`.

### Upload a file

`POST /api/v1/buckets/{id}/files`

Uploads a file asynchronously using `multipart/form-data`.

| **Field** | **Required** | **Description** |
| --- | --- | --- |
| `file` | Yes | The file bytes. For private folders, upload the Seal-encrypted object. |
| `name` | No | File name, up to 255 characters in length. |
| `contentSize` | No | Plaintext byte count for a ciphertext upload, so Console can show the original size. |
| `metadata` | No | JSON object string persisted with the file. Maximum 8 KiB. |

Returns `202` with the file summary under `data`. Poll the status endpoint until the job state is `completed`, at which point the file status is `active`. When the name already exists in the folder, Console renames the new file to `<name> (1).<ext>` and still returns `202`.

> **Info**
>
> After finalize, the onchain access grant takes a few seconds to propagate to the access index. During this window, uploads fail with a `403` and the error code `mirror_missing_grant`. Retry every few seconds until the grant appears.
Other responses include `404` (the folder is still in `pending_policy`), `413` (payload too large), `415` (unsupported content type), `422` (quota exceeded), and `429` (rate limited).

## Files

File endpoints address a single file by ID: metadata, upload status, download, signed download URLs, and delete.

### Get file metadata

`GET /api/v1/buckets/{id}/files/{fileId}`

Returns `200` with the file summary under `data`: `id`, `bucket_id`, `name`, `blob_id`, `oyster_object_id`, `size`, `content_size`, `mime_type`, `status`, `metadata`, `last_opened_at`, `created_at`, `updated_at`, and `deleted_at`. `status` is one of `pending`, `uploading`, `active`, `deleting`, `failed`, or `deleted`. `mime_type` is the type recorded at upload, which for a private folder is the type of the ciphertext.

### Update a file

`PATCH /api/v1/files/{id}`

Renames a file or edits its description and tags. This endpoint takes the file ID directly, not nested under a folder.

| **Field** | **Required** | **Description** |
| --- | --- | --- |
| `name` | No | New file name, up to 255 characters in length. |
| `description` | No | Free text, up to 1000 characters in length. Pass `null` to clear. |
| `tags` | No | Up to 10 tags, each up to 30 characters in length. Pass `null` to clear. |

Send at least one field. Unknown fields return `400` with code `bad_request`. Returns `200` with the updated file summary under `data`.

### Delete a file

`DELETE /api/v1/buckets/{id}/files/{fileId}`

Marks the file as `deleting` and enqueues a background job to remove the underlying blob. Returns `204` immediately. The worker releases storage after confirming the delete. Only one delete job runs per file, and a second `DELETE` for the same file ID returns `404`.

### Get upload status

`GET /api/v1/buckets/{id}/files/{fileId}/status`

Polls the state of an in-flight upload after the upload endpoint returns `202`.

Returns `200` with `data.state`, one of `queued`, `active`, `completed`, or `failed`. When the worker reports it, `data.progress` gives fractional progress from 0 to 1. When the state is `failed`, `data.error` includes a `code` and `message`. Completed and failed jobs stay readable for a while, then Console evicts them and this endpoint returns `404`. A `404` does not mean the upload failed. If you get a `404` after uploading, read the file metadata endpoint and treat a file status of `active` as success.

### Download a file

`GET /api/v1/buckets/{id}/files/{fileId}/download`

Returns `307` with a `Location` header that points at a short-lived signed URL on the user-content host, `https://files.walrususercontent.com`. Most HTTP clients follow the redirect automatically, and `curl` needs `-L`. The bytes and headers come from the redirect target, which serves the content as `application/octet-stream` with `Content-Disposition: attachment`, regardless of the file's recorded MIME type. Read `mime_type` from the file metadata when you need it.

Clients drop the `Authorization` header on the redirect hop, which is correct. The token in the signed URL is the credential, and the API key must never reach the user-content host. Do not configure your client to forward the header.

For a private folder, the bytes are the Seal ciphertext, which you decrypt client-side. See the [Quick Start](./quickstart) for the decrypt flow.

A file in `deleting` or `deleted` status returns `423`.

### Mint a signed download URL

`POST /api/v1/buckets/{id}/files/{fileId}/download-url`

Mints the same kind of signed URL directly, for when you need a link to hand to a browser or a third party. It works with `read_only` keys and the URL lasts 15 minutes. Returns `200` with `data.download_url`, a root-relative path to resolve against the base URL, and `data.expires_at`. A file that is not `active` returns `409`.

## Search

### Search files

`GET /api/v1/search`

Searches files across the key's space, with optional filters and facets. A `read_only` key can call this endpoint.

| **Parameter** | **Location** | **Required** | **Description** |
| --- | --- | --- | --- |
| `q` | query | No | Search text, up to 200 characters in length. |
| `types` | query | No | Up to 10 file categories: `CODE`, `GEO`, `IMAGE`, `AUDIO`, `DOCUMENT`, `TABLE`, `TEXT`, `VIDEO`, `ARCHIVE`, or `OTHER`. |
| `tags` | query | No | Up to 20 tags. |
| `sizeMin` / `sizeMax` | query | No | Size bounds in bytes. |
| `modifiedFrom` / `modifiedTo` | query | No | Modification time bounds. |
| `scope` | query | No | `space` or `bucket`. Pass the matching UUID in `scopeId`. |
| `sortField` | query | No | `modified`, `name`, or `size`. |
| `sortDirection` | query | No | `asc` or `desc`. |
| `limit` | query | No | Page size, 1 to 100. |
| `cursor` | query | No | Cursor from a previous page. |

Returns `200` with `data.files`, `data.folders`, `data.facets.categories`, `data.total_count`, and a `pagination` object. A validation failure returns `400` with code `bad_request`.

## Seal

These endpoints build and sponsor the onchain bucket-policy transactions that manage private access, and proxy Seal key fetches for decryption. Console sponsors the gas through [Enoki](https://docs.enoki.mystenlabs.com/), so your service key signs but does not need a token balance. The [Quick Start](./quickstart) drives them through the SDK.

### Sponsor a transaction

`POST /api/v1/seal/sponsor`

Builds a sponsored bucket-policy transaction and returns the bytes to sign plus a digest to echo to the execute endpoint. The body's `kind` selects the operation. Two kinds are callable with an API key:

| **`kind`** | **Purpose** | **Additional fields** | **Who can call it** |
| --- | --- | --- | --- |
| `bucket_group_create` | Create the access group for a reserved folder. | `bucketId` | Any key, but only for a folder that same key just reserved. The create-folder response already returns these bytes, so most callers never call this kind directly. |
| `grant_bucket_access` | Grant access to a recipient. | `groupIds` (1 to 1000), `recipientAddress`, `scope` (`read` or `readwrite`) | `key_admin` keys only. |

Other kinds exist for the Console web app and return `403` to every API key.

Returns `200` with `bytes` and `digest`. Console binds the digest to the calling key for a limited time. An invalid `kind` or a missing field returns `400` with the raw validator output.

### Execute a sponsored transaction

`POST /api/v1/seal/sponsor/{digest}/execute`

Submits the signature over the sponsored bytes. Console broadcasts the transaction and returns the onchain digest.

| **Parameter** | **Location** | **Required** | **Description** |
| --- | --- | --- | --- |
| `digest` | path | Yes | The sponsor digest from the sponsor endpoint. |

Body:

```json
{ "signature": "<base64 signature>" }
```

Returns `200` with the onchain `digest`. An unknown or expired digest, or a digest minted by a different key, returns `410` with code `sponsor_digest_expired`.

### Fetch a Seal key

`POST /api/v1/seal/aggregator/v1/fetch_key`

Proxies the Seal key-server aggregator so a client can decrypt without holding an aggregator credential. You do not call this endpoint by hand. Point the Seal SDK at `https://api.console.walrus.xyz/api/v1/seal/aggregator` as the aggregator URL and pass your API key as the bearer token. The SDK appends `/v1/fetch_key` and sends the key request during `decrypt`. A `read_only` key can call this endpoint.

The request body is capped at 180 KiB. The endpoint allows 60 requests per minute per account and 120 per minute per IP address, and a denied request returns `429` with `Retry-After`. Errors that Console generates, such as authentication and rate-limit failures, carry a `message` of the form `console:<code>`, which is the one field the Seal SDK keeps. Errors from the aggregator pass through unchanged.