API Reference
The Walrus Console developer API covers the endpoints third-party developers use to manage spaces, folders, and files, plus the Seal endpoints that back private-folder access control.
The Console developer API is at https://api.console.walrus.xyz. For a guided walkthrough of the full encrypted flow, start with the Quick Start. For the product model behind these endpoints, see the concepts and overview.
Authentication
Every request carries an API key as a bearer token:
Authorization: Bearer hbr_…
Mint keys through the Console web interface. Each key carries one of three scopes:
read_writekeys can change state: create and delete folders, upload and delete files, and finalize private folders.read_onlykeys can list, check status, mint download URLs, and download. Any other write with a read-only key returns403with coderead_only_api_key.key_adminkeys belong to Management API keys. They reach only the API-key endpoints and the Seal sponsor endpoints, and every data-plane endpoint returns403. This page covers the data-plane keys. For programmatic key minting, see the MCP server.
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:
{ "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:
{ "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. |
401 | api_key_rotation_incomplete | The key was revoked by a rotation that did not finish. Create a new key in Console. |
401 | api_key_replaced | The key was rotated and a new key replaces it. Use the new API key and service private key shown in Console. |
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. |
Rotating revokes the old key before the new one is shown, so there is no window in which both work.
Requests that exceed a limit return 422 with a distinct shape:
{
"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.
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. scope accepts private only.
Body:
{ "name": "secrets", "scope": "private" }
Returns 201 with the reservation:
{
"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 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 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:
{ "signature": "<base64 signature>" }
Returns 200:
{ "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.
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 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, so your service key signs but does not need a token balance. The Quick Start 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:
{ "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.