Install. Quickstart. Reference.
Get the CLI on your machine, the SDK in your project, and the API in your head, in that order.
Two paths in.
The CLI for daily uploads, scripting, and ingest. The SDK when you're writing code against the API.
Command-line client.
curl -fsSL https://khaosstorage.com/install | sh
Drops khaos-storage and the khs alias on your PATH. Re-run to upgrade. Requires Node 20+.
TypeScript client.
npm i https://khaosstorage.com/sdk/latest.tgz
Same package we'll publish to npm later. Works in Node and any modern bundler.
Three steps to first asset.
Sign in, upload, share. The CLI version is one terminal pane; the Node version is six lines of code.
Authorize once, in the browser.
khs login
Pick files interactively.
khs upload
Publish into a space.
khs upload showreel.mov \ --space "Highlights"
Pass an API key.
import { KhaosClient } from
'@khaosstorage/sdk';
const client = new KhaosClient({
baseUrl: 'https://api.khaosstorage.com',
apiKey: process.env.KHAOS_API_KEY,
});
Multipart, hashed, retried.
import { sourceFromPath }
from '@khaosstorage/sdk/node';
const src = await sourceFromPath(
'./clip.mov');
const asset = await client.upload(
src);
Filter and read back.
const recent = await
client.assets.list({
type: 'video',
limit: 20,
});
The shape of the API.
Every endpoint takes Authorization: Bearer <key>, returns
{ data, meta?, error? }, and is gated by a scope on the calling key.
Errors carry a code, message, and (when applicable) a
fields array.
Base URL
https://api.khaosstorage.com
Scopes
read · write · admin. Higher scopes inherit lower.
Errors
HTTP status, plus error.code like insufficient_scope, validation_error, not_found.
Assets
Upload, list, get, update, delete, and resolve signed download URLs. The upload helpers in the SDK and CLI choreograph this resource end to end.
POST /v1/assets write
Create an asset and receive an upload descriptor (single PUT for ≤100 MiB, multipart above). The SDK's client.upload(...) wraps this plus the part PUT and complete calls.
khs upload clip.mov
await client.upload(
await sourceFromPath('clip.mov')
);curl -X POST https://api.khaosstorage.com/v1/assets \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"filename":"clip.mov",
"type":"video",
"content_type":"video/quicktime",
"size_bytes":3621090133,
"multipart":true
}'GET /v1/assets read
List assets. Supports limit, cursor, status, type, search, and modified_after.
Node
const assets = await client.assets.list({
type: 'video',
status: 'active',
limit: 50,
});
GET /v1/assets/{asset_id} read
Fetch one asset by id. The response carries any populated thumb_url and preview_url as 1-hour signed GETs.
await client.assets.get('ast_…');
PATCH /v1/assets/{asset_id} write
Update tags, filename, or attributes. Returns the full asset.
await client.assets.patch('ast_…', { tags: ['raw','drone'] });
DELETE /v1/assets/{asset_id} write
Hard-delete the asset and its R2 objects (original, thumb, preview). Idempotent.
await client.assets.delete('ast_…');
GET /v1/assets/{asset_id}/url read
Mint a signed download URL for the original. Default 1-hour TTL via expires_in.
await client.assets.url('ast_…', 3600);
POST /v1/assets/{asset_id}/parts write
Mint a per-attempt presigned PUT URL for one part of a multipart upload. Refresh per attempt; URLs expire in 15 minutes.
await client.assets.presignPart('ast_…', uploadId, partNumber);
POST /v1/assets/{asset_id}/complete write
Finalize the upload. Pass upload_id + parts for multipart; empty body for single PUT. Triggers the processor fan-out.
await client.assets.completeUpload('ast_…', uploadId, parts);
POST /v1/assets/{asset_id}/abort write
Abort an in-flight multipart upload. Cleans up R2 state. Called automatically by the SDK when an upload is cancelled.
await client.assets.abortUpload('ast_…', uploadId);
POST /v1/assets/{asset_id}/reprocess write
Re-run the image / video processor. Useful after a processor bug fix or to regenerate thumbnails.
await client.assets.reprocess('ast_…');
GET /v1/assets/{asset_id}/spaces read
List the spaces an asset is currently published into.
await client.assets.spaces('ast_…');
Spaces
A space is how you share. Public = anyone with the URL. Protected = anyone with the URL plus a password. Assets are always private; publishing into a space is what surfaces them externally.
POST /v1/spaces admin
Create a space. visibility is public or protected; the latter requires a password.
await client.spaces.create({
name: 'Highlights',
visibility: 'public',
});
GET /v1/spaces read
List your spaces.
await client.spaces.list();
GET /v1/spaces/{space_id} read
Get one space, including asset_count.
await client.spaces.get('spc_…');
PATCH /v1/spaces/{space_id} admin
Update name, description, visibility, or password. Pass password: null to clear.
await client.spaces.update('spc_…', { visibility: 'public', password: null });
DELETE /v1/spaces/{space_id} admin
Delete the space. Cascades publications. Underlying assets are not affected.
await client.spaces.delete('spc_…');
POST /v1/spaces/{space_id}/publications write
Publish one or more assets into the space. Returns per-asset success / error.
await client.spaces.publish('spc_…', ['ast_…','ast_…']);
GET /v1/spaces/{space_id}/publications read
List the assets published into a space, decorated with signed thumb/preview URLs.
await client.spaces.listPublications('spc_…');
DELETE /v1/spaces/{space_id}/publications/{asset_id} write
Unpublish one asset from a space. The asset itself stays.
await client.spaces.unpublish('spc_…','ast_…');
API keys
Long-lived bearer tokens for scripts, ingest agents, and integrations. Created with a scope (read / write / admin). The raw key is shown once at creation.
GET /v1/api-keys read
List your keys. Hashes only; never the raw value.
await client.apiKeys.list();
POST /v1/api-keys admin
Mint a new key. The response includes the raw key exactly once. Persist it now.
await client.apiKeys.create({
name: 'ingest-bot',
scopes: ['write'],
});
DELETE /v1/api-keys/{key_id} admin
Revoke. Idempotent. Subsequent requests bearing the revoked key fail at the authorizer.
await client.apiKeys.revoke('key_…');
Storage connections
Bring your own R2 or S3-compatible storage to extend capacity. Credentials live encrypted at rest under a per-account KMS key; nothing is logged or replayable.
GET /v1/storage-connections read
List configured connections.
POST /v1/storage-connections admin
Add a connection. The server runs HEAD / LIST / PUT / DELETE probes against the bucket before persisting.
POST /v1/storage-connections/preview write
Run the same probes without persisting. Useful for the onboarding wizard.
POST /v1/storage-connections/list-buckets write
Discovery: enumerate buckets visible to the credentials. Used during connection setup.
POST /v1/storage-connections/create-bucket admin
Discovery: create a new bucket using the credentials. Region-aware.
Account
The account record for the calling owner.
GET /v1/account read
Read the current account: display name, email, plan, status.
PATCH /v1/account admin
Update display name or email.
Usage
Plan utilization. Bytes and counts, split by Khaos-managed vs BYOK.
GET /v1/usage read
Returns total_assets, total_bytes, and the same broken out by managed_* and byok_*.
Public spaces
Unauthenticated. The viewer endpoints used by share links. Public spaces resolve immediately; protected spaces require a password unlock.
GET /v1/public/spaces/{short_id} none
If the space is public, returns the manifest with signed asset URLs. If protected, returns { requires_password: true }.
POST /v1/public/spaces/{short_id}/access none
Submit { password }; on match, returns the manifest. No tokens, no sessions; re-auth per request.
The spec.
A live openapi.yaml is in the repository. Drop it into a Postman / Bruno / Insomnia / Stoplight collection and you have a request playground.
openapi.yaml is being brought current with v0.1 — link added once published.
Get in touch.
Found a bug, want a feature, need access. We read everything.
Email k@khaos.studio.