Skip to main content

Quick Start

This quickstart takes you from sign-up to a working encrypted upload: create an account, mint an API key, then create a Seal-encrypted bucket and upload, download, and decrypt a file.

info

The Console developer API is in alpha and currently available on Testnet only, hosted under the Harbor name at api.testnet.harbor.walrus.xyz. Endpoint shapes might change before it reaches Mainnet at GA. In alpha, all bucket creation goes through the private, Seal-encrypted flow; public bucket creation is disabled at the API boundary. For the full endpoint surface, see the API reference; for the product model, see the concepts and overview.

Prerequisites

  • A Google account for sign-in.
  • Node.js with @mysten/sui and @mysten/seal installed, for the signing and encryption steps.

Every request below carries your API key as a bearer token:

Authorization: Bearer hbr_…
info

The code in this guide is pulled directly from the runnable walrus-harbor-quickstart example, so the package IDs and Seal key-server IDs stay current with the source. Clone it and run the pnpm scripts to try the flow end to end.

Sign up and create an API key

  1. Visit the Walrus Console app and sign in with Google. zkLogin provisions your account and a Personal Space automatically.

  2. Open Settings → API Keys → New API key, give it a name, pick role read_write, and tick Create so the key is encryption-capable. Submit.

  3. On the reveal screen, copy both secrets. Console shows them once and cannot recover them afterward:

    • hbr_…, the API key you send as Authorization: Bearer … on every request.
    • suiprivkey1…, the service private key: an Ed25519 secret in Sui keytool format, bound to this API key. Console stores only the derived public address, and it does not need a token balance. You use it to sign the finalize transaction and to authenticate decrypt sessions with Seal.

    Store both like cloud secret access keys. Paste them into your .env as your bearer token and HARBOR_SERVICE_PRIVKEY, or into Postman.

A read_only key (listing, status, and download only) is also available for downstream consumers that should not change your data; every write endpoint returns 403 with code read_only_api_key for those keys. The encrypted flow below needs the read_write encryption-capable key created above.

Create an encrypted bucket and upload a file

Private buckets are encrypted client-side, so Console stores ciphertext only and never sees your plaintext or decryption material. With your key in hand, creation goes through a reserve, sign, and finalize handshake, then a local encrypt on upload and decrypt on download:

get space → reserve → sign → finalize → encrypt → upload → poll → download → decrypt

Step 1: Get your space ID

GET /api/v1/spaces

The response's data[] array holds your spaces. Copy the id of the Personal Space created at sign-up.

Step 2: Reserve the bucket

POST /api/v1/spaces/{spaceId}/buckets
Content-Type: application/json

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

Response (201):

{
"bucket_id": "…",
"bytes": "<base64 sponsored Sui transaction>",
"digest": "…",
"state": "pending_policy"
}

bytes is the sponsored Sui transaction that creates the bucket's Seal access policy, with your service key's address as the sender. Console has already attached the gas sponsor's signature, so your service key only needs to add its own. digest is the sponsor digest, which Console uses at finalize to look up the sponsored transaction. The bucket stays in pending_policy and accepts no uploads until finalize succeeds.

The example code

The signing, encryption, and decryption steps below come from the runnable walrus-harbor-quickstart example. Two files hold everything: config.ts pins the Testnet package and Seal key-server IDs, and lib/seal.ts wraps the signing and Seal encrypt/decrypt helpers. Clone the repo, copy app/.env.example to app/.env, and set HARBOR_SERVICE_PRIVKEY to your service private key.

Step 3: Sign the bytes with the service key

Sign the reserved bytes with your service key. sign-reserve.ts loads the key with loadKeypair and calls signReserveBytes, returning the base64 signature you pass to finalize. Run it with pnpm sign-reserve <base64-reserve-bytes>:

Step 4: Finalize

POST /api/v1/buckets/{bucketId}/finalize
Content-Type: application/json

{ "signature": "<base64 signature from step 3>" }

Console combines your signature with the gas-sponsor signature and broadcasts the transaction. Response (200):

{ "bucket_id": "…", "seal_policy_id": "…", "state": "active" }

seal_policy_id is the onchain bucket-policy object ID that Seal uses for access checks. The bucket is now usable.

Step 5: Encrypt the file with Seal

Encrypt locally against the seal_policy_id from finalize. encryptBytes (in lib/seal.ts) derives a per-file Seal identity and encrypts against the bucket policy using ORIGINAL_PACKAGE_ID from config.ts. Seal pins identity derivation to the original package ID, so this value must stay fixed across package upgrades, otherwise an upgrade would invalidate every previously encrypted blob's key. encrypt-file.ts wires it up; run it with pnpm encrypt-file <plaintextPath> <sealPolicyId>:

It writes <plaintextPath>.enc, the byte stream you upload next.

Step 6: Upload

POST /api/v1/buckets/{bucketId}/files
Content-Type: multipart/form-data

file=@<encryptedObject>

Just after finalize, the onchain access grant needs a few seconds to propagate into Console's access index. Until it does, this endpoint returns 403 with code mirror_missing_grant. Retry every few seconds; usually 20 attempts is plenty in practice. Once the grant mirrors, the response is 202 with data.id.

Step 7: Poll status

GET /api/v1/buckets/{bucketId}/files/{fileId}/status

Returns data.state, one of queued, active, completed, or failed. Poll every second or two until the state is completed. Completion is typically under 30 seconds on Testnet.

Step 8: Download and decrypt

GET /api/v1/buckets/{bucketId}/files/{fileId}/download

The response is the raw Seal ciphertext. decryptBytes (in lib/seal.ts) builds the seal_approve access-check transaction, signs it with a short-lived session key, and decrypts client-side. decrypt-file.ts runs the download-to-plaintext round trip and checks the result against the original; run it with pnpm decrypt-file <ciphertextPath> <sealPolicyId>:

Decryption is fully client-side and never touches Console's backend.

Get help

  • Ask the team and other developers in the Walrus Discord.
  • File an issue on the walrus-harbor-quickstart example repo. Include the endpoint and method, the HTTP status, and the code field from any error response.