Base URL: https://api.infiniteocean.io — secure by default
Drops-first
Everything InfiniteOcean can do, with examples you can copy and run. The Drops language is the simplest way to interact with all features. Standard web requests still work too.
Full Drops language reference →All examples use the Drops language. Full reference →
-- 1. Save data save my/data @user-1 name: "Alice" email: "[email protected]" -- 2. Get it back get my/data @user-1 -- 3. Find by field find my/data where name = "Alice" -- 4. Full-text search search my/data "alice" top 5 -- 5. Or just use plain words for complex queries find my/data where name = "Alice" then pick name, email
// JavaScript — send Drops via POST /exec const res = await fetch("https://api.infiniteocean.io/exec", { method: "POST", headers: { "Content-Type": "text/plain", "X-Ocean-Key": "YOUR_KEY" }, body: 'save my/data @user-1 name: "Alice"' }); const result = await res.json(); // SSE streaming still uses HTTP (live events) const events = new EventSource("https://api.infiniteocean.io/stream/my/*?last=10"); events.onmessage = (e) => console.log(JSON.parse(e.data));
# Save data curl -X POST https://api.infiniteocean.io/exec \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: YOUR_KEY" \ -d '{"query":"save my/data @user-1 name: \"Alice\" email: \"[email protected]\""}' # Get it back curl -X POST https://api.infiniteocean.io/exec \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: YOUR_KEY" \ -d '{"query":"get my/data @user-1"}' # Find by field curl -X POST https://api.infiniteocean.io/exec \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: YOUR_KEY" \ -d '{"query":"find my/data where name = \"Alice\" then pick name, email"}'
Save information. Watch for changes in real time.
-- Save a drop (event) save myapp/events/signup user: "alice" plan: "pro" -- Save a keyed entity (versioned) save myapp/events/signup @latest user: "alice" plan: "pro" -- Save with TTL (auto-remove after 3600 seconds) save myapp/sessions @sess-1 user: "alice" keep 3600
Live updates — replay history and subscribe to new changes as they happen:
| URL | Behavior |
|---|---|
/stream/myapp/*?last=5 | Last 5 drops, then live |
/stream/myapp/*?from=0 | Full history replay, then live |
/stream/myapp/events/signup | Exact route, live only |
// SSE stream format data: {"drop_id":"...","route":"...","payload":"...","created_at":"..."} event: caught-up data: {"timestamp":"...","replayed":2} data: {"drop_id":"...","route":"...","payload":"...","created_at":"..."} ← live
Search by words or by meaning. Handles typos automatically. Your text is auto-indexed — just save data and search.
-- Full-text search (keyword matching) search myapp/logs "deployment error" top 10 -- Search with minimum score search myapp/docs "getting started" top 5 -- Write a vector drop, then search by vector save vectors/memory/conversations vector: (0.12, -0.34, 0.56, 0.78) text: "hello world"
Three modes: text (keyword search), vector (meaning-based search), hybrid (both combined). The Drops search command uses text mode. For vector and hybrid search, use the HTTP request directly.
Hybrid fusion: "weighted" (default) blends keyword and meaning scores evenly. "rrf" uses rank-based fusion — more robust when mixing different types of results. Great for building AI-powered search.
Bring your own embeddings: InfiniteOcean auto-embeds text with a built-in model, but you are not locked in. Pass a vector array of any size when writing data, and search with your own vectors. Use OpenAI, Cohere, Voyage, or any embedding provider — InfiniteOcean stores and indexes whatever you send. You can even mix: auto-embedded text search on one folder, custom OpenAI vectors on another.
// Response format { "results": [ { "drop_id": "a1b2c3d4-...", "route": "myapp/logs/deploy", "score": 3.2451, "payload": { "message": "deployment error on prod" }, "created_at": "2026-02-06T12:00:00.000Z" } ], "searched": 1200, "time_ms": 2, "mode": "text" }
Find, sort, and summarize your data using simple filters.
-- Filter by payload field find myapp/events where type = "signup" sort created_at desc limit 10 -- Numeric comparisons find myapp/orders where amount > 100 sort amount desc -- Text contains find myapp/logs where message contains "error" limit 20 -- Count results with then find myapp/events where type = "signup" then count -- Pick specific fields find myapp/users then pick name
Aggregation and joins are available for advanced use cases like grouping, summing, averaging, counting, and combining data from multiple folders. Or use SQL:
-- Group and aggregate via SQL find myapp/orders group category sum amount count -- Join via SQL find orders join users on user_id = key then pick *, name
Built-in functions: $sum, $avg, $min, $max, $count, $first, $last, $distinct
Six optional, well-known fields that let you search across all your data at once. Add them to any record and instantly find things by date, location, identity, or amount — no matter where they are stored. Full reference →
-- Write a drop with universal fields save work/meetings @standup-42 title: "Weekly Standup" _dates: ({ start: "2026-03-15T09:00:00Z" }) _identifiers: ( { type: "email", value: "[email protected]", role: "organizer" }, { type: "email", value: "[email protected]", role: "attendee" } ) _locations: ({ lat: 55.68, lon: 12.57, name: "Copenhagen" }) -- Cross-route query: find everything involving bob in March primitives dates { $gte: "2026-03-01", $lt: "2026-04-01" } identifiers { $has: { type: "email", value: "[email protected]" } } -- With location filter primitives dates { $gte: "2026-03-01" } locations { $near: { lat: 55.68, lon: 12.57, radius_km: 10 } }
The six fields — all start with _ and hold lists of values:
| Field | What it holds | Queryable by |
|---|---|---|
_texts | Strings. Automatically included in smart search. | search |
_dates | Objects with start, optional end, tz, label. | Date range |
_identifiers | Objects with type + value, optional name, role. | Exact, type, value, OR |
_locations | Objects with lat + lon, optional name. | Geo proximity |
_amounts | Objects with value, optional unit, label. | Range per unit |
_blobs | Objects with url + mime, optional name, size. | Filter on payload |
All filters are combined together. Results include records from any folder you have access to. You can also combine smart field filters with regular query filters.
Add a @key to any write to create a versioned record. Writing again with the same folder + key creates a new version. Delete by writing null. Full history is always preserved.
-- CREATE save users @user-123 name: "Alice" email: "[email protected]" -- READ get users @user-123 -- UPDATE (write again with same key) save users @user-123 name: "Alice Updated" email: "[email protected]" -- DELETE (tombstone) remove users @user-123 -- LIST all current entities (deduped, deleted excluded) list users -- PATCH (partial update — merge fields, unmentioned preserved) update products @sku-42 price: 29.99 on_sale: true -- Remove a field by patching null update products @sku-42 on_sale: null
Version history — every write creates a new version. Use GET /entity/:route?key=:key&history=true to list all versions, or &at=<timestamp> to fetch a specific historical version.
Auto-parsing: Stored data is automatically structured on retrieval.
Ask questions about your data using familiar query syntax. Field mapping is automatic: name in a query maps to the right place internally. Special fields key, created_at, drop_id, route, source pass through directly.
-- Save, get, update, remove save users @alice name: "Alice" email: "[email protected]" users @alice update users @alice name: "Alice V2" remove users @alice -- Find with filters find users where age > 21 and status = "active" find users where name starts with "A" find users where role in ("admin", "editor") find users where email exists -- Group and aggregate find orders group category sum amount count -- Combine data from two routes find orders join users on user_id = key then pick *, name -- Sort and paginate find users sort name limit 10 skip 20 -- Define schemas (optional) define "users" fields { name: "text required", email: "text", age: "number" } status users -- Remove a schema definition -- Atomic batch (all succeed or all fail) bulk (save ledger @tx1 amount: 100, save ledger @tx2 amount: -100)
Everything starts open so you can try things quickly. Lock it down when you're ready.
Secure any folder with a _access record. Four modes: shared (restrict who can read and write), protected (anyone can read, only approved people can write), writeonly (anyone can write, only approved people can read), or moderated (anyone can read and write, only moderators can delete).
Folders starting with _ (single underscore): _memory, _cron, _mcp, etc. Anyone can read, but writing requires your key. First writer owns the record — others cannot overwrite it.
Folders named after your key are automatically private. Only you can read and write. You can share access to specific apps.
Give apps access to specific parts of your private data. You share, you unshare. Your data stays under your control.
Folders starting with shared/. A trust record at trust/{name} controls who can access shared/{name}/*.
Folders starting with trust/ are system-managed. First write establishes ownership. Direct reads are blocked.
Folder permissions — use share and unshare to control access to any folder. Four modes: shared (reads + writes restricted), protected (anyone reads, restricted writes), writeonly (anyone writes, restricted reads), and moderated (anyone reads and writes, only moderators delete). Permissions inherit downward — locking agency/shop also protects agency/shop/products.
-- Lock down a route (shared mode — both reads and writes restricted) share agency/shop with @YOUR_KEY mode shared -- Protected mode — public reads, restricted writes share agency/shop with @YOUR_KEY mode protected -- Writeonly mode — anyone can write, only granted keys can read -- Use case: signup forms, feedback, anonymous submissions share feedback/inbox with @YOUR_KEY mode writeonly -- Moderated mode — anyone can post, only moderators can delete -- Use case: community chat, forums, comments, reviews share community/general with @MODERATOR_KEY mode moderated -- Add another key to a route's grant list share agency/shop with @CLIENT_KEY -- Remove access revoke agency/shop from CLIENT_KEY -- Remove ACL entirely (only the owner can do this) revoke agency/shop
Permissions: stream, search, query, entity, sql, export (read operations), drop (write). Each controls a specific capability.
Private folders: Folders starting with your public key are automatically private. Grant access to apps without revealing your key.
Adopt any key (device, agent, service) from your personal key. Auto-grants you full access to the claimed key's private data.
List all your claimed keys, check ownership, update labels. One account, many devices.
Hand a device to another owner in one call. Old grants revoked, new grants written atomically.
Unclaim a key to revoke your access. The device keeps full access to its own data.
# Claim a device key curl -X POST https://api.infiniteocean.io/key/claim \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: $KEY" \ -d '{"key":"DEVICE_KEY","label":"Living room camera","type":"device"}' # List all keys you own curl -s https://api.infiniteocean.io/key/claimed \ -H "X-Ocean-Key: $KEY" # Check who owns a key curl -s https://api.infiniteocean.io/key/owner/DEVICE_KEY # Read device data using your key (auto-granted) curl -s https://api.infiniteocean.io/entity/DEVICE_KEY/sensors/temperature?key=latest \ -H "X-Ocean-Key: $KEY" # → 200 # Transfer ownership to another key curl -X POST https://api.infiniteocean.io/key/transfer \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: $KEY" \ -d '{"key":"DEVICE_KEY","to":"NEW_OWNER_KEY"}' # Release ownership curl -X DELETE https://api.infiniteocean.io/key/claim/DEVICE_KEY \ -H "X-Ocean-Key: $KEY"
How it works: Claiming automatically grants you access to the device's data. The device keeps its own access unchanged. Transferring moves ownership to someone else in one step. Releasing removes your access.
All operations are free — reads, writes, search, SQL, functions. No per-request charges. Every node on the network contributes automatically, so workloads are distributed. io credits are only used for priced content (records with a price field).
Saving, reading, searching, running code, querying, exporting — all free. No io cost for any operation.
Set a price on any record to sell access. Buyers pay in io, and 99% goes directly to you. Great for APIs, datasets, templates, or premium content.
# Anonymous curl https://api.infiniteocean.io/whoami → { "tier": "anonymous", "ip": "..." } # With key curl https://api.infiniteocean.io/whoami -H "X-Ocean-Key: your-key" → { "tier": "balance", "key": "your-key", "balance": 947 }
io balances are used for purchasing priced content (records with a price field). All operations — search, SQL, functions — are free regardless of balance. Send io to another user, or receive io from others.
Transfer io by @handle, email, or public_id.
| Field | Description |
|---|---|
| to | Recipient: @handle, email, or public_id (required) |
| amount | Whole number of io to send (required) |
curl -X POST https://api.infiniteocean.io/io/send \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: your-key" \ -d '{"to":"@bob","amount":100}' → { "ok": true, "sent": 100, "to": { "handle": "@bob" }, "remaining": 850 }
Add a price field to any record to require payment before the full content is visible. Non-purchasers see a preview but the content is hidden. 99% of each purchase goes to you, 1% to the platform.
-- Write a drop with a 5 io price tag save myapp/premium @article-1 "full content here" price 5 -- Non-purchaser reads it → payload stripped get myapp/premium @article-1 -- { drop_id: "...", price: 5, _gated: true } -- Purchase via HTTP: POST /purchase/<drop_id>
Set a price on your function's _config record. Callers are charged upfront — 99% goes to you, 1% to the platform.
-- Set a 10 io price on your function save myapp/fn/premium @_config execute: "public" price: 10 -- When someone runs: run myapp/fn/premium { input } -- 10 io transferred from caller to you (99/1 split)
Every io movement — transfers, charges, grants — is permanently recorded. View your transaction history anytime.
| Param | Description |
|---|---|
| limit | Max entries (default 100, max 1000) |
| offset | Skip first N entries (default 0) |
curl https://api.infiniteocean.io/ledger?limit=10 \ -H "X-Ocean-Key: your-key" → { "entries": [ { "type": "transfer", "amount": 100, "from": "you", "to": "bob", "fee": 1, "net": 99, "from_bal": 900, "to_bal": 1099, "txn_id": "..." }, { "type": "drain", "amount": 1, "from": "you", "from_bal": 899, "txn_id": "..." } ], "count": 2 }
Types: transfer (P2P send or purchase), topup (Stripe purchase). You only see your own transactions.
Upload any file. Stream video and audio. File information is stored alongside your other data, so files are searchable and queryable like everything else.
Send the raw file as the request body. Folder and content type go in headers.
| Header | Description |
|---|---|
| Content-Type | MIME type of the file (image/png, video/mp4, audio/mpeg, etc.) |
| X-Blob-Route | Folder for the file's metadata record. |
| X-Ocean-Key | Optional. Your key for private folders. |
# Upload a video curl -X POST https://api.infiniteocean.io/blob \ -H "Content-Type: video/mp4" \ -H "X-Blob-Route: myapp/videos" \ --data-binary @movie.mp4
// Response { "drop_id": "a1b2c3d4-...", "route": "myapp/videos", "source": "base64-public-key", "blob": { "key": "blobs/a1b2c3d4-...", "size": 15728640, "type": "video/mp4" }, "created_at": "2026-02-07T12:00:00.000Z" }
Max size per single request: 100 MB. For larger files, use chunked uploads.
Upload files of any size in 10 MB chunks. Resumable — if a connection drops, check progress and continue from where you left off.
| Field | Description |
|---|---|
| route | Folder for the file's metadata record. |
| type | MIME type of the file. |
| size | Total file size in bytes. |
| name | File name. |
# 1. Start the upload curl -X POST https://api.infiniteocean.io/upload/start \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: your-key" \ -d '{"route":"myapp/videos","type":"video/mp4","size":524288000,"name":"big-video.mp4"}' → { "upload_id": "uuid", "part_size": 10485760, "total_parts": 50 } # 2. Upload each part (10 MB chunks) curl -X PUT https://api.infiniteocean.io/upload/uuid/part/1 \ -H "Content-Type: application/octet-stream" \ --data-binary @chunk-1.bin → { "part": 1, "etag": "...", "remaining": 49 } # 3. Check progress (for resume) curl https://api.infiniteocean.io/upload/uuid → { "upload_id": "uuid", "total_parts": 50, "completed_parts": [1, 2, 3] } # 4. Complete when all parts are uploaded curl -X POST https://api.infiniteocean.io/upload/uuid/complete → { "drop_id": "uuid", "route": "myapp/videos", "blob": { "size": 524288000, ... } }
Returns the file with its original type. Supports seeking in video and audio players.
# Stream a video (full) curl https://api.infiniteocean.io/blob/a1b2c3d4-... -o video.mp4 # Range request (bytes 0-1023) curl -H "Range: bytes=0-1023" https://api.infiniteocean.io/blob/a1b2c3d4-... → HTTP 206 Partial Content
// Play in a browser — just use the URL <video src="https://api.infiniteocean.io/blob/a1b2c3d4-..." controls></video> <audio src="https://api.infiniteocean.io/blob/a1b2c3d4-..." controls></audio> <img src="https://api.infiniteocean.io/blob/a1b2c3d4-...">
Blob responses include Cache-Control: public, max-age=31536000, immutable and Accept-Ranges: bytes. (Blobs are content-addressed by UUID — they never change.)
// JavaScript — upload + embed const file = document.querySelector('input[type="file"]').files[0]; const res = await fetch("https://api.infiniteocean.io/blob", { method: "POST", headers: { "Content-Type": file.type, "X-Blob-Route": "myapp/uploads", }, body: file, }); const { drop_id } = await res.json(); // Now playable forever video.src = `https://api.infiniteocean.io/blob/${drop_id}`;
See it in action — this video is served from InfiniteOcean:
Find your files — each upload creates a record with file information, so you can search, query, or use SQL to find files by folder, time, or any details you included.
Client libraries: ocean-rtc.js enables peer-to-peer video and audio calls through InfiniteOcean. One-to-one calls via OceanRTC, group rooms via OceanRoom. No media server needed.
Stream raw binary data from blob drops as a continuous HTTP stream. Works directly as an <audio> or <video> source in all browsers — no JavaScript needed.
| Param | Description |
|---|---|
| ?type= | Override content type (e.g. audio/mpeg). Auto-detected by default from the blob's .type file or codec payload field. |
<!-- Use it directly as an audio or video source --> <audio src="https://api.infiniteocean.io/listen/myapp/radio" controls></audio> <video src="https://api.infiniteocean.io/listen/myapp/stream" controls></video> <!-- Override content type --> <audio src="https://api.infiniteocean.io/listen/myapp/radio?type=audio/mpeg" controls></audio> <!-- Private route --> <audio src="https://api.infiniteocean.io/listen/myapp/radio?key=YOUR_KEY" controls></audio>
On connect, the most recent segment plays immediately. New segments are piped as they arrive via chunked transfer encoding. Content type is auto-detected from the blob's .type file, the codec field in the payload, or falls back to application/octet-stream. Each blob drop must have a blob_id field in its payload.
Sign people in with email. No passwords needed — they click a link and they're in. For social login (Google, GitHub, Apple), verify the email through your preferred provider, then resolve it to an Ocean Key. No passwords, no sessions, no cookies.
Send a sign-in email. If the email is new, an Ocean key is created and stored. If returning, the same key is used. The key is never revealed in this response.
| Field | Description |
|---|---|
| Required. Normalized to lowercase. | |
| callback_url | Optional. Magic link points here instead of InfiniteOcean. Your backend verifies the token. |
| app_name | Optional. Shown in the email heading and "Authenticated by InfiniteOcean" footer. |
| subject | Optional. Custom email subject. Defaults to "Sign in to {app_name}" or "Sign in to InfiniteOcean". |
# Direct — link goes to API, returns JSON curl -X POST https://api.infiniteocean.io/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]"}' # As auth provider for your app — link goes to your domain curl -X POST https://api.infiniteocean.io/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","callback_url":"https://yourapp.com/auth/callback","app_name":"YourApp"}' → { "sent": true }
Rate limit: 5 requests per email per hour. With callback_url, the magic link points to callback_url?token=... — your backend calls GET /auth/verify?token=... to get the key.
Verify a magic link token and get the Ocean key. Called by your backend after the user clicks the link. Token is single-use and expires after 15 minutes.
curl https://api.infiniteocean.io/auth/verify?token=abc123... → { "key": "base64url-ocean-key" }
Look up the email and account details for a key obtained via magic link.
curl https://api.infiniteocean.io/auth/me \ -H "X-Ocean-Key: your-key" → { "email": "[email protected]", "key": "...", "tier": "authenticated" }
Probe-only: tells you whether an account exists for the given email. Does not return a key, does not create an account, does not send mail. Used by OAuth flows to decide between "first-time signup" and "returning user" branches before redirecting through the magic-link flow.
Behavior changed 2026-04-29: previously returned ocean_key; now returns only existence + public_id. Account creation happens at /auth/verify (magic-link click), not here. No X-Ocean-Key header required.
| Field | Description |
|---|---|
| Required. The email address to probe. Normalized to lowercase. |
# If account exists → { "is_new": false, "public_id": "a1b2c3d4e5f6" } # If account does not exist → { "is_new": true }
# Resolve a verified email → Ocean Key curl -X POST https://api.infiniteocean.io/auth/resolve \ -H "Content-Type: application/json" \ -H "X-Ocean-Key: developer-key" \ -d '{"email":"[email protected]"}' → 201: { "ocean_key": "...", "public_id": "a1b2c3d4e5f6", "is_new": true } → 200: { "ocean_key": "...", "public_id": "a1b2c3d4e5f6", "is_new": false }
Requires X-Ocean-Key header (developer key). Rate limit: 20 resolves per key per hour. Returns 201 for new users, 200 for existing.
For full OAuth tutorials with Google and GitHub, see Social Login Tutorial.
// Magic link flow await fetch("https://api.infiniteocean.io/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "[email protected]", callback_url: "https://yourapp.com/auth/callback", app_name: "YourApp" }) }); // Verify token → get key const token = new URLSearchParams(location.search).get("token"); const { key } = await (await fetch(`https://api.infiniteocean.io/auth/verify?token=${token}`)).json(); // OAuth flow — resolve a verified email directly const { ocean_key } = await (await fetch("https://api.infiniteocean.io/auth/resolve", { method: "POST", headers: { "Content-Type": "application/json", "X-Ocean-Key": DEVELOPER_KEY }, body: JSON.stringify({ email: verifiedEmail }) })).json();
For CLIs and AI agents that don't have email, the platform supports a headless bootstrap path: POST /drop without an X-Ocean-Key header generates a fresh Ed25519 keypair server-side and returns both halves once in the response. Save both halves at that moment — they will not be returned again.
# Bootstrap a keypair without email curl -X POST https://api.infiniteocean.io/drop \ -H "Content-Type: application/json" \ -d '{"route":"hello","payload":"first drop"}' → { "drop_id": "...", "route": "hello", "source": "<public>", "keypair": { "public": "<public>", "private": "<private>" } }
Crypto-conscious callers (hardware-backed keys, etc.) can opt out of server-side keypair generation with ?generate_keypair=false — anonymous calls then return 401 KEY_REQUIRED and the caller must generate the keypair locally. The server briefly holds the private key in memory during response generation; it is never persisted. The TLS connection is the only wire-side protection. If you can't tolerate that window, generate locally and pass X-Ocean-Key.
Register a URL to be notified whenever new data is saved to a folder. When matching data is written, the full record is sent to your URL automatically.
Optional signature verification: provide a secret when registering to verify that notifications genuinely come from InfiniteOcean.
Each trigger has an owner (the key that registered it). Only the owner can list or delete it.
Timeout: 10 seconds per attempt. Retries: none (fire-and-forget). URL: must be HTTPS. Delivery is in write order per folder. The full record is sent in the request body.
Toggle triggers on/off with "active": false without deleting.
-- React to changes when orders changes call "https://my-server.com/hook" -- With webhook secret when orders changes call "https://my-server.com/hook" secret "s3cret" -- List webhooks webhooks -- Delete a webhook webhook delete "hook-id-here"
Use add and subtract for counters that are safe even with many simultaneous updates. No conflicts, no matter how many requests happen at once.
Counter updates are sub-millisecond. Counter values are automatically included in reads, queries, and SQL results.
-- Add to a counter add 1 to pages @home views -- Add a larger amount add 5 to pages @home views -- Subtract subtract 1 from pages @home stock -- Read — counter value is merged in get pages @home -- { title: "Home", views: 5 } -- Counter value is merged in add 1 to pages @home views
Record lookups by key are automatic and instant, no matter how much data you have.
For specific fields you query often, create an index to speed things up even more.
# Entity index is automatic — instant reads GET /entity/users?key=alice ← instant lookup, any dataset size # Create a field index POST /exec {"query":"status users"} # Indexes are created automatically on frequently-queried fields → { "command": "CREATE_INDEX", "table": "users", "field": "email" } # Now queries on email are index-accelerated POST /exec {"query":"find users where email = \"[email protected]\""} ← reads only matching entities, skips full scan # Show and drop indexes POST /exec {"query":"status users"} → { "columns": ["table","field"], "rows": [["users","email"]] }
Save HTML and view it in a browser. Named records give you permanent URLs — update the record, the page updates. Instant publishing with full version history.
Served from view.infiniteocean.io — isolated for security. File type is detected from the key extension (.css, .js, .png, etc.) or defaults to HTML.
Privacy works too — private and shared folders require ?_key= to view. Priced records return 402 until purchased.
All view responses include these headers automatically:
Cache-Control: public, no-cache — always revalidates, page updates visible immediatelyReferrer-Policy: strict-origin-when-cross-originX-Frame-Options: SAMEORIGIN — prevents clickjackingX-Content-Type-Options: nosniffCSP is not set by default — the platform is multi-tenant and a global CSP breaks sites that use external resources (Google Fonts, CDNs, etc.).
To add CSP to your site, save a _config entity on your view route with a csp field:
save mysite @_config csp: "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src * data: blob:; connect-src 'self' https://api.infiniteocean.io"
The Content-Security-Policy header will appear on all pages under that route. Remove it by tombstoning: save mysite @_config null
Use href="style.css" in your HTML — it just works. Directory-style URLs auto-redirect to add a trailing slash (/mysite → /mysite/), so relative asset paths resolve correctly.
Keys can contain / (e.g. pages/about). The system automatically detects this and finds the right record.
If a record is accidentally deleted, the page still serves the previous live version. Your pages stay up even if data is removed — only direct lookups show "deleted".
Map your own domain to a folder. Add a CNAME pointing to view.infiniteocean.io (or an A record for apex domains), register it, and your domain serves your pages with automatic HTTPS.
DNS setup: Add a CNAME record, wait for propagation (usually minutes), then visit your domain — HTTPS is set up automatically on first visit. For apex domains, use an A record pointing to InfiniteOcean's IP.
# Write an HTML page POST /drop {"route":"mysite","key":"index","payload":"<!DOCTYPE html><html>..."} # View it in a browser https://view.infiniteocean.io/mysite/ ← serves the HTML https://view.infiniteocean.io/mysite/index ← same thing # Add CSS and JS as separate drops POST /drop {"route":"mysite","key":"style.css","payload":"body { color: #fff }"} https://view.infiniteocean.io/mysite/style.css ← text/css # Keys with "/" — works in View URLs POST /drop {"route":"mysite","key":"pages/about","payload":"<html>..."} https://view.infiniteocean.io/mysite/pages/about ← found! # Custom domain — 3 steps POST /domains {"domain":"mycoolsite.com","route":"mysite"} # 1. Add CNAME: mycoolsite.com → view.infiniteocean.io # 2. Wait for DNS propagation (usually minutes) # 3. Visit https://mycoolsite.com/ — TLS auto-provisioned
Export all records under a folder as JSON, CSV, SQL, or MongoDB. Default is streaming NDJSON (one record per line). Pick a format and get a file ready for your target system.
Supports latest=true for deduplication, filter= for filtering, from=/to= for time ranges, and select= for choosing specific fields.
Formats:
Types are inferred automatically: TEXT, BIGINT, BOOLEAN, DOUBLE PRECISION, JSONB for Postgres; VARCHAR, BIGINT, TINYINT, DOUBLE, JSON for MySQL.
# HTTP export — pick a format GET /export/users?latest=true → NDJSON (default) GET /export/users?latest=true&format=csv → CSV file GET /export/users?latest=true&format=sql → PostgreSQL GET /export/users?latest=true&format=sql&dialect=mysql → MySQL GET /export/users?latest=true&format=mongo → MongoDB script -- Drops syntax export users → JSON (default) export users as csv → CSV export users as sql → PostgreSQL export users as sql dialect mysql → MySQL export users as mongo → MongoDB
-- Example SQL output
CREATE TABLE IF NOT EXISTS "users" (
"_key" TEXT PRIMARY KEY,
"_created_at" TIMESTAMPTZ,
"name" TEXT,
"role" TEXT,
"active" BOOLEAN
);
INSERT INTO "users" ("_key","_created_at","name","role","active") VALUES
('alice','2026-01-01T12:00:00Z','Alice','admin',TRUE);
See how any Drops command maps to SQL, curl, or REST. Your logic is never locked in — translate it to standard tools at any time.
Targets: sql (PostgreSQL), mysql, curl, rest
Commands: find, save, remove, get, search, count
-- Query → SQL translate "find users where .role = 'admin' sort .name limit 10" to sql → SELECT * FROM "users" WHERE "role" = 'admin' ORDER BY "name" LIMIT 10; -- Write → SQL (upsert) translate "save users @alice name: 'Alice'" to sql → INSERT INTO "users" ... ON CONFLICT ("_key") DO UPDATE SET ...; -- Delete → SQL translate "remove users @alice" to sql → DELETE FROM "users" WHERE "_key" = 'alice'; -- Any command → curl translate "find users where .active = true" to curl → curl -X POST https://api.infiniteocean.io/query -H ... -d '...' -- MySQL dialect translate "find users where .role = 'admin'" to mysql → SELECT * FROM `users` WHERE `role` = 'admin';
Use list or find with pipes to transform, filter, and act on data:
-- Drops: list entities and pipe results list users then count list users then pick name list users then first list users then sort name list users then limit 10 -- Pipe actions: send email, save backup, remove find customers then sort created_at desc then first then send email to email subject "Welcome!" body "Thanks for joining." find users then first 10 then save backups/users @snapshot find old/logs then remove
Write code and run it. Your code can read, write, search, and query your data. It gets an ocean client that can do everything you can do from the outside.
The Ocean client uses your key, so your functions have the same access as you do — read your folders, write your data, query your records.
Sync mode: 10 second timeout. Free.
Async mode: Add async: true for long-running functions. Poll with status command. Also free.
-- Store a function save fn/hello @main "def main(ocean, request): return {'hello': request.get('name', 'world')}" -- Run it run fn/hello { name: "Ocean" } -- { result: { hello: "Ocean" }, ms: 3 } -- Run async (long-running) run fn/process { data: "..." } async -- { job_id: "abc123", status: "running" } -- Poll status status "abc123"
Function execution is free. No io cost regardless of runtime.
| Method | Description |
|---|---|
ocean.read(route, key) | Read an entity |
ocean.write(route, payload, key) | Write a drop |
ocean.query(prefix, filter, sort, limit) | Query drops |
ocean.search(prefix, text, vector, top_k) | Search |
ocean.sql(query) | Run SQL |
ocean.run(route, body) | Call another function |
ocean.upload(route, data, filename) | Upload a file |
ocean.export_entities(route) | Export all entities as a list |
All methods raise OceanError on errors. Uncaught errors propagate as execution failures.
Store a _config record alongside your function to enable code privacy and data isolation. The server automatically tracks function ownership so it cannot be forged.
Code privacy: Store functions in your private folder. Nobody can read the code. With execute:"public", anyone can run it.
Two-key model: When _config exists, ocean uses the developer's key (app state), and ocean.user uses the caller's key, limited to declared folders.
Public functions: When a function has execute:"public", anyone can call it — even without a key. Ideal for public services, notifications, and AI tools.
-- Store a private function save fn/greet @main "def main(ocean, request): return {'hello': request.get('name')}" -- Enable public execution with scoped user access save fn/greet @_config execute: "public" scopes: ("myapp/users") -- Anyone can now run it — code stays private run fn/greet { name: "world" } -- Set a price on your function (10 io per call) save fn/premium @_config execute: "public" price: 10
Connect your AI assistant to your InfiniteOcean functions. Claude Desktop, Cursor, VS Code, and other AI tools can call your functions directly. One config record defines the connection — tools are regular InfiniteOcean functions.
Stateless: Each request is independent. No sessions to manage.
Free: All AI tool connection operations are free — tool listing, initialization, and function execution.
Custom servers: Write a config record at _mcp/servers with tool definitions. Each tool maps to an InfiniteOcean function.
-- 1. Create a tool function save my-tools/summarize @main "def main(ocean, request): url = request['url'] # ... fetch and summarize ... return {'summary': text}" -- 2. Create MCP server config save _mcp/servers @my-server { name: "My MCP Server", version: "1.0.0", app_key: "YOUR_OCEAN_KEY", tools: { summarize: { description: "Summarize a URL", inputSchema: { type: "object", properties: { url: { type: "string" } } }, function: "my-tools/summarize" } } } -- Now connectable at POST /mcp/my-server
/mcp/oceanEvery account gets a built-in AI tool connection at POST /mcp/ocean with 4 core tools. No setup needed — just connect with your Ocean Key.
Primary tool: exec — executes Drops commands. This single tool gives access to every feature: write, read, query, search, SQL, functions, scheduled tasks, triggers, email, calendar, notifications, and more.
Other tools: get_context (full project context), remember (save dev notes), recall (search dev notes).
Auth: Pass your key as ?key= in the URL or via X-Ocean-Key header.
Cost: Everything is free.
Claude.ai: Add as a custom connector — paste the URL with your key, no OAuth needed.
# Claude Desktop / Cursor { "mcpServers": { "ocean": { "url": "https://api.infiniteocean.io/mcp/ocean", "headers": { "X-Ocean-Key": "YOUR_KEY" } } } } # VSCode / GitHub Copilot (.vscode/mcp.json) { "servers": { "ocean": { "type": "sse", "url": "https://api.infiniteocean.io/mcp/ocean?key=YOUR_KEY" } } } # Claude.ai custom connector URL https://api.infiniteocean.io/mcp/ocean?key=YOUR_KEY # 4 tools: # exec — execute Drops (replaces 25 individual tools) # get_context — full project context + memory # remember — save dev notes to _memory # recall — search dev notes in _memory # # The exec tool accepts any Drops command: # write, read, delete, list, patch, query, # search, sql, run, status, cron, on, grant, # revoke, mail, event, events, rsvp, push, # validate, define, patterns, primitives, ...
Run any function on a schedule. Standard schedule syntax: minute hour day month weekday. Scheduled tasks run the same way as on-demand function calls.
Your ocean key is stored with the task, so scheduled functions have the same data access as your regular calls.
All scheduled tasks are free — no cost to run.
-- Run daily at 9am every day at 9am daily-report: run fn/report { to: "team" } -- Run every 5 minutes every 5 minutes health-check: run fn/ping -- List scheduled jobs schedules -- Remove a schedule schedule delete "daily-report"
Connections combine data, functions, triggers, and schedules — no extra infrastructure needed. Connect Stripe, Slack, GitHub, SendGrid, or anything with a web service.
Incoming triggers: External services send events to /webhook/:name. The raw event is captured as a record in _webhooks/{name}. An outbound trigger fires your handler function to process and route the event.
Polling triggers: Use scheduled tasks to call an external service on a timer. Your function tracks progress and saves new items.
Actions: Functions that call external services. Other functions can invoke them directly.
Workflows: Chain triggers, logic, and actions together using outbound triggers between functions.
Credentials: Store secret keys in your private folder ({KEY}/credentials/{name}). Your functions can read them securely.
Discovery: Connection registrations are records in _integrations, searchable via SQL.
# 1. Register an integration POST /drop {"route":"_integrations", "key":"my-stripe", "payload":"{\"type\":\"stripe\",\"active\":true}"} # 2. External service sends events here: # POST /webhook/my-stripe # → drop written to _webhooks/my-stripe # → outbound webhooks fire # 3. Deploy a handler function POST /drop {"route":"my-app/stripe-handler","key":"main", "payload":"def main(ocean, request):\n event = json.loads(request['body'])\n ocean.write('stripe/charges', event['id'], event)"} # 4. Wire handler to webhook drops POST /hooks {"url":"https://api.infiniteocean.io/run/my-app/stripe-handler", "prefix":"_webhooks/my-stripe"} # 5. Store credentials (private route) POST /drop {"route":"YOUR_KEY/credentials/stripe", "key":"webhook_secret", "payload":"whsec_..."} # Discover integrations POST /exec {"query":"find _integrations"}
When your app stores user data, privacy rules require the ability to erase, hide, and manage how long data is kept. InfiniteOcean handles this at the storage level — real deletion everywhere, not just hiding data behind a flag.
Right to erasure: Permanently deletes all records matching a folder prefix. Files are physically removed from all storage. Indexes are rewritten without the erased entries.
Selective redaction: Replaces specific fields with [REDACTED] across all matching records. Original values are destroyed — history is rewritten too.
Retention policies: Sets an auto-deletion window (in days) for a folder prefix. Records older than the retention period are automatically purged.
All three operations require your key with admin access and a prefix parameter to scope the operation.
Important: folder names and keys are structural
InfiniteOcean uses a tamper-proof chain for auditing. When data is erased, the content is physically deleted and the record becomes unsearchable — but the folder name, key, and timestamp remain as structural information. This means folder names and keys must never contain personal data. Use opaque identifiers (users/u_8f3a) instead of real names (users/john-doe). Keep all personal information inside the content, where it can be fully erased or hidden.
# Right to erasure — delete all user data POST /gdpr/erase {"prefix": "users/user-123"} # Response {"erased": 47, "prefix": "users/user-123"} # Field-level redaction — remove PII POST /gdpr/redact {"prefix": "users/user-123", "fields": ["email", "phone", "address"]} # Response {"redacted": 12, "fields": ["email","phone","address"]} # Set retention policy — auto-delete after 90 days POST /gdpr/retention {"prefix": "logs/analytics", "retention_days": 90} # Response {"prefix": "logs/analytics", "retention_days": 90}
Full email built in. Claim a mailbox, send and receive email, add custom domains. Every email is stored as a record with smart fields — searchable, queryable, and works with everything else.
Get started: Open the Dashboard, go to the Mail tab, and claim your username. Or use Drops below.
Claim a subdomain: provisions [email protected] automatically. All email authentication is configured for you. Costs 100 io.
Unlimited addresses: Any local part works — hello@, invoices@, support@ all route to the same inbox.
Inbound: Received emails appear as records in mail/{username}/inbound with full body, headers, and smart fields.
Custom domains: Register your own domain via POST /mail/domain (HTTP). Add DNS records at your registrar, then verify. Costs 500 io.
-- Send an email mail to "[email protected]" subject "Hello from the ocean" body "Hey Bob!" -- Send with from address mail from "[email protected]" to "[email protected]" subject "Hello" body "Hey Bob!" -- Read your inbox find mail/alice/inbound sort created_at desc limit 20 -- Search your mail search mail/alice/inbound "invoice" top 10 -- List your domains (HTTP) -- GET /mail/my-domains -- Claim mailbox (HTTP) -- POST /mail/claim { username: "alice" } -- Custom domains (HTTP) -- POST /mail/domain { domain: "mycoolsite.com" } -- POST /mail/domain/verify { domain: "mycoolsite.com" }
Reference content from other records with {"$embed": {...}} in your data. The server resolves them when you read — the original creator keeps ownership, view tracking, and monetization.
Like YouTube embeds: you reference, they serve. The linked record remains fully owned by the original creator — their counters increment, their pricing applies, and the link is always live.
-- 1. Create source content save photos/nature @sunset url: "https://cdn.example.com/sunset.jpg" -- 2. Embed it in a gallery (use $embed in payload) save gallery @featured { title: "Featured", hero: { "$embed": { route: "photos/nature", key: "sunset" } } } -- 3. Read — hero is auto-resolved get gallery @featured -- payload.hero = { $embedded: true, mode: "data", -- payload: { url: "https://cdn..." } } -- Live embed — skip 5-min cache save dashboard @main { ticker: { "$embed": { route: "data/feed", key: "ticker", live: true } } }
| Field | Type | Description |
|---|---|---|
route | string | Source record's folder (required) |
key | string | Source record's key (required) |
mode | string | "data" (default, full content) or "view" (URL + summary only) |
live | boolean | Bypass 5-minute cache for real-time freshness |
| Field on source | Model | Behavior |
|---|---|---|
price | One-time | Reader pays once via POST /purchase, embeds resolve forever |
price_per_read | Per-read | Each time the linked content is loaded, the reader is charged. Creator earns continuously. |
?embed_depth=N resolves N levels deep (default 1, max 5)?resolve=false returns raw $embed objects"resolve": true to POST /query bodyembed_views auto-increments on the source recordAny record with start + title is a calendar event. Calendar feeds work on any folder — subscribe from Google Calendar, Apple Calendar, or Outlook. Invite emails include native Accept/Decline buttons. Responses are saved on the event.
Events live on whatever folder you choose. Calendar is a convention, not a separate system — the same query, search, and smart fields you use for everything else work on events too.
-- Create an event with invites event calendar/team { title: "Sprint planning", start: "2026-03-10T10:00:00Z", end: "2026-03-10T11:00:00Z", location: "Room 3", organizer: "[email protected]", attendees: ( { email: "[email protected]" }, { email: "[email protected]" } ), send_invites: true } -- List events on a route events calendar/team -- List events with date range events calendar/team start "2026-03-01" end "2026-04-01"
Set send_invites: true with an organizer and attendees to send invite emails. Gmail, Outlook, and Apple Calendar show native Accept/Decline buttons.
Calendar feeds: Subscribe to any folder from your calendar app. The feed URL is available via the calendar feed command.
-- RSVP to an event rsvp calendar/team @sprint-42 "[email protected]" accepted -- Get calendar feed URL calendar feed calendar/team -- { feed_url: "https://api.infiniteocean.io/ -- calendar/feed/calendar/team?token=abc..." } -- Paste feed_url into Google Calendar: -- Settings → Add calendar → From URL
| Field | Type | Description |
|---|---|---|
title | string | Event title (required) |
start | string | ISO 8601 start time (required). Date-only for all-day events. |
end | string | ISO 8601 end time |
description | string | Event description |
location | string | Event location |
organizer | string | Organizer email (required for invites) |
attendees | array | [{ "email": "...", "status": "needs-action" }] |
rrule | string | Recurrence rule (e.g. FREQ=WEEKLY;BYDAY=MO) |
status | string | confirmed, tentative, or cancelled |
sequence | number | Auto-incremented on update (tracks event changes for calendar apps) |
Native browser push notifications built in. No Firebase, no external service — everything runs inside InfiniteOcean.
Two models: explicit send (notify specific users by key) and folder subscriptions (auto-notify when new data is saved to a matching folder — like triggers but delivered to the browser).
Expired subscriptions are automatically cleaned up. Free — no io charge.
-- Send a push notification push send { to: ("target-ocean-key"), title: "New message", body: "You have a new message", url: "https://myapp.com/messages/123" } -- Subscribe to route notifications push subscribe myapp/orders -- List your subscriptions push subscriptions -- Unsubscribe push unsubscribe "subscription-id"
Browser subscription registration uses standard browser APIs:
// Register service worker + subscribe to push const reg = await navigator.serviceWorker.register('/sw.js'); const vapid = await fetch('https://api.infiniteocean.io/push/vapid-key') .then(r => r.json()); const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapid.publicKey) }); // Send subscription to InfiniteOcean const { endpoint, keys } = sub.toJSON(); await fetch('https://api.infiniteocean.io/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Ocean-Key': YOUR_KEY }, body: JSON.stringify({ endpoint, keys }) });
Some data is useful in the moment but not historically — a now-playing track, presence pings, sensor readings, typing indicators. Real-time streams broadcast to live listeners without persisting anything: no chain entry, no manifest, no S3, no disk. Full reference →
Two ways to use it:
POST /drop on it is auto-routed to broadcast. Existing client code keeps working; the platform just stops persisting.Listeners subscribe via SSE on GET /broadcast/:route/listen. Cross-server fan-out is automatic — a listener on any node receives broadcasts published on any node.
Need a permanent record of a specific session? Toggle recording on for a stream-type route and drops are also persisted under :route/recordings/:key until you toggle it off. Best of both worlds: ephemeral by default, durable when you want it.
// Mark a route as a stream (one-time setup) POST /drop { "route": "_config/myapp/now-playing", "key": "stream", "payload": { "type": "stream" } } // Publish (existing /drop client code, no change needed) POST /drop { "route": "myapp/now-playing", "payload": { "track": "Whatever - Whoever" } } // → {"ok":true,"stream":true,"recording":false} // Listen (browser) const ev = new EventSource( 'https://api.infiniteocean.io/broadcast/myapp/now-playing/listen?key=' + KEY ); ev.onmessage = (e) => console.log(JSON.parse(e.data)); // Record this episode POST /record/start { "route": "myapp/now-playing", "key": "ep-42" } // drops now ALSO land in myapp/now-playing/recordings/ep-42/ POST /record/stop { "route": "myapp/now-playing" } // back to ephemeral
Create your own currency or token. Any user can mint a new value with a unique symbol, then distribute it to other keys. Built-in ledger tracks every mutation.
Transfer fee: 0.1% platform fee on every transfer. The sender pays the fee on top — the recipient always receives the exact amount. Example: transfer 100, sender pays 100 + 1 fee, recipient gets 100.
Scope: public (anyone can receive) or private (only existing holders or the creator can receive transfers).
Supply can be capped with max_supply. Use decimals for fractional tokens (e.g. decimals: 2 means 100 = 1.00).
// Create a currency value create "gold" name "Gold" symbol "GLD" // With options value create "gold" name "Gold" symbol "GLD" max_supply 1000000 decimals 2 scope "private" // Mint (creator only) value mint "gold" 1000 value mint "gold" 500 to "recipientKey" // Transfer (0.1% fee) value transfer "gold" 100 to "recipientKey" // Burn from your balance value burn "gold" 50 // Check balance, supply, holders, ledger value balance "gold" value supply "gold" value currencies value holders "gold" value ledger "gold"
// HTTP endpoints
POST /values/create { id, name, symbol, scope, max_supply, decimals }
POST /values/mint { currency, amount, to }
POST /values/transfer { currency, amount, to }
POST /values/burn { currency, amount }
GET /values/balance?currency=X&holder=Y
GET /values/currencies?creator=X
GET /values/currency/:id
GET /values/holders?currency=X
GET /values/supply?currency=X
GET /values/ledger/:id?limit=N
Define the shape of your data with named fields and validation rules. Attach HTML templates that render records automatically. Structured data that validates on save and displays on read.
Patterns are regular records in the patterns folder. View templates are HTML records referenced by drop:// URIs. The view system at view.infiniteocean.io resolves patterns, loads templates, and renders the final HTML.
-- Define a pattern (simple) define todo fields title: "text" status: "text" due: "date" -- Define with detailed rules (braces for nesting) define todo version "1" fields { title: { type: "text", required: true }, status: { type: "text", enum: ("pending", "done") }, due: { type: "date" }, tags: { type: "text", array: true } } -- List all patterns patterns
Check data against a pattern before saving. Returns { valid, errors }.
Validation checks: required fields present, values match declared types, enum fields contain allowed values, array fields are arrays.
-- Validate data against a pattern validate todo title: "Ship v2" status: "pending" -- { valid: true, errors: [] } -- Save a validated record save todos @ship-v2 pattern: "todo" title: "Ship v2" status: "pending" due: "2026-04-01" tags: ("release", "important") -- View at: view.infiniteocean.io/todos/ship-v2
When a record has a pattern field, the view system renders it with a template:
patterns/{name}pattern.views[mode]Template syntax: {{field}} for values, data-ref="field" for references, data-array="field" for lists. Elements with unfilled placeholders are automatically hidden.
View modes: ?mode=full (default) and ?mode=preview (compact).
-- Write a view template save views @"todo/full" "<article><h1>{{title}}</h1><p>{{description}}</p><time>{{due}}</time><div data-array='tags'>Tags</div></article>"
| Type | Description | Options |
|---|---|---|
text | Free-form text string | required, array, enum |
date | ISO 8601 date or datetime | required, array |
identifier | Email, URL, phone, or other ID | required, array |
blob | Binary data reference (file path or URL) | required, array |
location | Geographical location (lat/lon or address) | required, array |
amount | Numeric amount with optional unit | required, array |
relation | Reference to another entity ($embed) | required, array |
| Situation | Code | Status |
|---|---|---|
| Missing folder when saving | ROUTE_REQUIRED | 400 |
| Invalid folder format | INVALID_ROUTE | 400 |
| Invalid record key | INVALID_KEY | 400 |
| Missing key when reading a record | KEY_REQUIRED | 400 |
| Invalid search prefix | INVALID_PREFIX | 400 |
| Missing both vector and text | SEARCH_REQUIRED | 400 |
| Invalid vector format | INVALID_VECTOR | 400 |
| Invalid text format | INVALID_TEXT | 400 |
| Query dimensions != stored dimensions | DIMENSION_MISMATCH | 400 |
| SQL parse or execution error | SQL_ERROR | 400 |
| Data does not match the expected format | SCHEMA_VIOLATION | 400 |
| top_k exceeds 1000 | TOP_K_TOO_LARGE | 400 |
| Record not found | NOT_FOUND | 404 |
| Record has been deleted | DELETED | 404 |
| No vectors at this location | NO_VECTORS | 404 |
| No text content at this location | NO_TEXT_CONTENT | 404 |
| Missing key on a private folder | KEY_REQUIRED | 401 |
| Key does not have access | FORBIDDEN | 403 |
| No trust record for shared folder | NO_TRUST | 403 |
| Content over 10MB | PAYLOAD_TOO_LARGE | 413 |
| Single file upload over 100MB (use chunked) | BLOB_TOO_LARGE | 413 |
| File not found | NOT_FOUND | 404 |
| Encoding job not found | NOT_FOUND | 404 |
| Encoding failed | ENCODE_ERROR | 500 |
| Invalid email on POST /auth/login | INVALID_EMAIL | 400 |
| Magic link token invalid or used | INVALID_TOKEN | 400 |
| Magic link token expired | TOKEN_EXPIRED | 400 |
| Too many logins for this email | AUTH_RATE_LIMITED | 429 |
| Not enough io balance (for priced content) | RATE_LIMITED | 429 |
| Too many key generations from IP | KEYGEN_LIMITED | 429 |
| Server at capacity | SERVER_BUSY | 503 |
| Server error | INTERNAL_ERROR | 500 |
Open folders are great for prototyping. Before production, lock them down with a single command:
grant my/route to YOUR_KEY protected
Drops auto-protects routes on first write. Use write public to opt out.
This is real — every command runs against InfiniteOcean using a shared demo account. State is shared with all visitors of this page.
// Click a button above...