InfiniteOceanv2

Reading data

Fetch one drop, page through a route, check a write's receipt, and list the routes your key can reach — including what complete:false actually means to a caller.

Reading data

Four read endpoints. GET /v2/entity/… fetches one drop by key. GET /v2/list/… walks a whole route, one page at a time. GET /v2/receipt/… tells you how far a write you just made has got. GET /v2/routes lists the routes your key can reach.

Every read needs your key in an X-Ocean-Key header. All examples below use $OCEAN_KEY; substitute your own.

export OCEAN_KEY='<your-key>'

The examples on this page run against a route that was populated first, with seven drops keyed city-001 … city-007:

curl -s -X POST https://api2.infiniteocean.io/v2/drop \
  -H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
  -d '{"route":"_docs_probe/eucities","key":"city-001",
       "payload":{"name":"Lisbon","country":"PT","pop":544851}}'

GET /v2/entity/{route}/{key}

Returns the current payload for one key. The response body is the payload JSON exactly as you stored it — there is no envelope, no ok field, no metadata. Content-Type is application/json.

Request

curl -s https://api2.infiniteocean.io/v2/entity/_docs_probe/eucities/city-003 \
  -H "X-Ocean-Key: $OCEAN_KEY"

Response — 200

{"name":"Oslo","country":"NO","pop":709037}

Payloads that are not objects

A payload does not have to be an object. Whatever JSON value you stored comes back as the whole body, unwrapped. Three drops written as 42, "hello" and [1,2,3] read back as:

curl -s https://api2.infiniteocean.io/v2/entity/_docs_probe/shapes/num -H "X-Ocean-Key: $OCEAN_KEY"
# 42
curl -s https://api2.infiniteocean.io/v2/entity/_docs_probe/shapes/str -H "X-Ocean-Key: $OCEAN_KEY"
# "hello"
curl -s https://api2.infiniteocean.io/v2/entity/_docs_probe/shapes/arr -H "X-Ocean-Key: $OCEAN_KEY"
# [1,2,3]

The same values appear inline under value in a list response.

How the path splits

Everything after /v2/entity/ is split at the last slash: what comes before is the route, what comes after is the key. Routes may therefore contain slashes freely — /v2/entity/_docs_probe/eucities/nested/mykey reads key mykey from route _docs_probe/eucities/nested.

Both halves are percent-decoded after that split, so a key containing a slash or a space works if you encode it. This request reads the key a b/c:

curl -s "https://api2.infiniteocean.io/v2/entity/_docs_probe/eucities/nested/a%20b%2Fc" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"note":"key with a space and a slash"}

Encoding the slashes in the route half works too — the split is done on the raw path, so /v2/entity/_docs_probe%2Feucities/city-003 returns the same drop as the unencoded form.

Errors

StatusBodyWhen
401{"ok":false,"error":"X-Ocean-Key required","code":"KEY_REQUIRED"}Header missing or empty.
401{"ok":false,"error":"unknown key — not authenticated","code":"UNKNOWN_KEY"}Key is not a key this server knows.
403{"ok":false,"error":"forbidden","code":"FORBIDDEN"}The route has no access rule granting your key read. A route that has never been written to also lands here — you get 403, not 404.
404{"ok":false,"error":"entity not found","code":"NOT_FOUND"}The route is readable but that key has no drop. Also what a read on a different node returns for a short window after a write — see Cross-node reads.
400{"ok":false,"error":"path does not name a valid resource","code":"BAD_PATH"}No key segment, e.g. /v2/entity/_docs_probe, or an empty key segment, e.g. /v2/entity/_docs_probe/eucities/.
404{"ok":false,"error":"no /v2 endpoint matches this method and path","code":"NOT_FOUND"}Nothing after the prefix at all, e.g. /v2/entity/.
400{"ok":false,"error":"unknown query parameter \"pretty\" — nothing was applied","code":"UNKNOWN_PARAM"}This endpoint takes no query parameters. Any parameter at all is a hard 400.

GET /v2/list/{route}

Returns one page of the latest value for every key in a route, sorted by key ascending. Always an envelope — never a bare array.

Request

curl -s https://api2.infiniteocean.io/v2/list/_docs_probe/eucities \
  -H "X-Ocean-Key: $OCEAN_KEY"

Response — 200

{"ok":true,
 "items":[
   {"key":"city-001","ts":1787593972810,"value":{"name":"Lisbon","country":"PT","pop":544851}},
   {"key":"city-002","ts":1787593973092,"value":{"name":"Porto","country":"PT","pop":231962}},
   {"key":"city-003","ts":1787593973393,"value":{"name":"Oslo","country":"NO","pop":709037}},
   {"key":"city-004","ts":1787593973695,"value":{"name":"Bergen","country":"NO","pop":286930}},
   {"key":"city-005","ts":1787593975110,"value":{"name":"Copenhagen","country":"DK","pop":644431}},
   {"key":"city-006","ts":1787593975413,"value":{"name":"Aarhus","country":"DK","pop":285273}},
   {"key":"city-007","ts":1787593975692,"value":{"name":"Helsinki","country":"FI","pop":658864}}],
 "returned":7,"complete":true,"index":true,"next_cursor":null}

Envelope fields

FieldTypeMeaning
itemsarrayOne object per key: key (string), ts (write time, ms since epoch), value (your payload JSON, inline).
returnednumberitems.length. Never larger than limit.
completeboolWhether the server can certify it showed you the whole route. Read the next section — this is the one field people get wrong.
indexboolUnfiltered lists only: whether a durable route index was found. false means the route is freshly written and the node is answering from its own recent-write state. On a filtered list (field+value) this field is always true and tells you nothing — see below.
next_cursorstring or nullThe last key on this page, to be passed back as cursor. null when the page was not filled.

Query parameters

NameDefaultNotes
limit201–100. 0, 101, a negative number and a non-number are all 400 BAD_LIMIT, not a clamp.
cursor—Exclusive: the page starts at the first key strictly greater than this string.
field—Filtered list. Must be sent together with value.
value—Filtered list. Must be sent together with field. An empty value= is 400 INVALID_INPUT.

Anything else — offset, sort, page — is a hard 400 with UNKNOWN_PARAM. Nothing is silently ignored.

What complete:false means, and how to page

complete:true is a strong claim: this route contains nothing you have not now seen. complete:false means the node is not certifying the answer. It does not mean the items you got are wrong; every item returned is real. It means "there may be more that I cannot see from here". Three situations produce it, and the envelope does not always separate them:

The rule for callers

next_cursor:null does not mean you have everything. A page can end with a null cursor and complete:false at the same time — that is the normal shape for a recently written route. Stop paging on next_cursor:null; decide whether to trust the result on complete.

#!/usr/bin/env bash
# Page a whole route. Stops on next_cursor:null; reports whether
# the last page certified completeness.
ROUTE=_docs_probe/eucities
CURSOR=""
COMPLETE=""
while :; do
  if [ -z "$CURSOR" ]; then
    PAGE=$(curl -s -G "https://api2.infiniteocean.io/v2/list/$ROUTE" \
             --data "limit=100" -H "X-Ocean-Key: $OCEAN_KEY")
  else
    PAGE=$(curl -s -G "https://api2.infiniteocean.io/v2/list/$ROUTE" \
             --data "limit=100" --data-urlencode "cursor=$CURSOR" \
             -H "X-Ocean-Key: $OCEAN_KEY")
  fi
  echo "$PAGE" | jq -c '.items[]'
  COMPLETE=$(echo "$PAGE" | jq -r '.complete')
  CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done
[ "$COMPLETE" = "true" ] || echo "WARNING: route not certified complete — retry later" >&2

If you need a certified-complete snapshot, re-run the walk until the final page reports complete:true. On a quiet route that happens within minutes of the last write; on a route being written continuously it may never happen, because a peer always has something in flight.

Paging worked example

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?limit=3" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[…city-001,city-002,city-003…],"returned":3,
#  "complete":false,"index":true,"next_cursor":"city-003"}

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?limit=3&cursor=city-003" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[…city-004,city-005,city-006…],"returned":3,
#  "complete":false,"index":true,"next_cursor":"city-006"}

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?limit=3&cursor=city-006" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[…city-007…],"returned":1,
#  "complete":true,"index":true,"next_cursor":null}

The cursor does not have to be a key that exists — it is a plain string comparison, so cursor=city-004a resumes at city-005. A cursor past the end of the route returns an empty page:

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?limit=3&cursor=zzzz" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[],"returned":0,"complete":true,"index":true,"next_cursor":null}

Keep keys URL-safe, or paging will silently skip rows

The cursor value is compared byte-for-byte against your keys without being percent-decoded first. If a key contains a character you must encode in a query string — a space, &, %, #, + — the cursor you send back does not match the key it came from, and rows are dropped with no error.

Measured. A route with exactly two keys, "s t" and "s!u":

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities/enc" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[{"key":"s t",…},{"key":"s!u",…}],"returned":2,
#  "complete":true,"index":true,"next_cursor":null}

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities/enc?limit=1" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[{"key":"s t",…}],"returned":1,
#  "complete":false,"index":true,"next_cursor":"s t"}

# Feed that cursor back, percent-encoded exactly as a query string requires:
curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities/enc?limit=1&cursor=s%20t" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[],"returned":0,"complete":true,"index":true,"next_cursor":null}

Key s!u exists — the full list above returns it, and GET /v2/entity/_docs_probe/eucities/enc/s%21u returns {"k":"s!u"} — but page two came back empty. Worse, it came back complete:true: the server certified a page that had silently skipped a row. Sending the cursor as s+t behaves identically. Sending a raw & is different again — it is read as a parameter separator, so ?cursor=x&y returns 400 {"ok":false,"error":"unknown query parameter \"y\" — nothing was applied","code":"UNKNOWN_PARAM"}.

There is no client-side workaround today. Restrict keys you intend to page over to A–Z a–z 0–9 - _ . ~. Keys outside that set are fine for GET /v2/entity/…, which does decode correctly.

Filtered list — field + value

Returns only the keys in the route whose payload has field equal to value, read out of a secondary index.

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?field=country&value=NO" \
  -H "X-Ocean-Key: $OCEAN_KEY"
{"ok":true,
 "items":[
   {"key":"city-003","ts":0,"value":{"name":"Oslo","country":"NO","pop":709037}},
   {"key":"city-004","ts":0,"value":{"name":"Bergen","country":"NO","pop":286930}}],
 "returned":2,"complete":true,"index":true,"next_cursor":null}

Four things to know before you rely on it:

limit and cursor page the filtered result the same way as an unfiltered list, and the cursor is a key from the filtered page:

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?field=country&value=PT&limit=1" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[{"key":"city-001","ts":0,"value":{"name":"Lisbon",…}}],"returned":1,
#  "complete":false,"index":true,"next_cursor":"city-001"}

curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/eucities?field=country&value=PT&limit=1&cursor=city-001" \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"items":[{"key":"city-002","ts":0,"value":{"name":"Porto",…}}],"returned":1,
#  "complete":true,"index":true,"next_cursor":null}

List errors

StatusCodeWhen
400BAD_LIMITlimit=0, limit=101, limit=-1, limit=abc. Message: limit must be a positive integer within the allowed range — nothing was applied.
400PARTIAL_FILTEROnly one of field/value.
400INVALID_INPUTfield set with an empty value=. Message: invalid filter — nothing was applied.
400UNKNOWN_PARAMAny parameter other than the four above.
401KEY_REQUIRED / UNKNOWN_KEYMissing or unrecognised X-Ocean-Key.
403FORBIDDENNo access rule grants your key read on the route — including routes that do not exist.
404NOT_FOUNDno /v2 endpoint matches this method and path — you used the wrong method (only GET is routed) or omitted the route, e.g. /v2/list/.

GET /v2/receipt/{seq}

Tells you how far a specific write has progressed. {seq} is the seq integer returned by POST /v2/drop.

curl -s -X POST https://api2.infiniteocean.io/v2/drop \
  -H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
  -d '{"route":"_docs_probe/receipts","key":"life-pinned","payload":{"n":2}}'
# {"ok":true,"seq":1787577342970126,"offset":0,"len":84,"ts":1787594387682,
#  "state":"accepted","rule_created":false,"drop_id":"1787577342970126",
#  "route":"_docs_probe/receipts","key":"life-pinned"}

curl -s https://api2.infiniteocean.io/v2/receipt/1787577342970126 \
  -H "X-Ocean-Key: $OCEAN_KEY"
# {"ok":true,"seq":1787577342970126,"state":"accepted","node":"hil"}

Polled every five seconds against the node that took the write, the same seq moved from accepted to durable between the first and second poll and stayed there:

+0s   {"ok":true,"seq":1787577342970126,"state":"accepted","node":"hil"}
+5s   {"ok":true,"seq":1787577342970126,"state":"durable","node":"hil"}
+11s  {"ok":true,"seq":1787577342970126,"state":"durable","node":"hil"}

Receipts expire

A receipt is not a permanent record. Once the write has settled, the entry is retired and the endpoint returns 404 for that seq on every node. Two writes were polled every five seconds from acceptance:

run 1 (polled through api2.infiniteocean.io, so the load balancer picks the node)
  +0s    accepted        +5s  durable   …   +375s durable   +381s 404
  Re-checked later against all five nodes with --resolve: 404 everywhere.

run 2 (polled with --resolve pinned to the accepting node)
  +0s    accepted        +5s  durable   …   still durable at +654s

Run 1's 404 at +381 s cannot be trusted as the moment of retirement — that poll went through the load balancer and may have landed on a node that never held the receipt at all. Run 2, pinned to the accepting node, was still answering durable at +654 s. This page therefore gives no retention number. What is established is that receipts do disappear, and that the seq from run 1 later returned 404 on all five nodes.

So poll a receipt while a write is in flight; do not store a seq and expect to resolve it later. A 404 from this endpoint is not evidence that a write failed — it is also what a settled receipt and a non-accepting node both return.

States

StateMeaning
acceptedThe write is on the accepting node's disk and will not be lost if that node restarts. It is not yet in object storage.
durableThe write has reached object storage. This is the state to wait for if you are gating anything on durability, and the last state observed before a receipt retires.

A third state, global, appears in the server's own vocabulary but was never produced in any probe behind this page. Do not build a wait loop that blocks on it. Treat durable as the terminal state you can rely on.

The key header is required but not checked

This endpoint refuses a request with no X-Ocean-Key (401 KEY_REQUIRED), but it does not resolve the key or check it against the write's owner. An unrecognised key gets the same 404 NOT_FOUND as an unknown seq, not 401 UNKNOWN_KEY:

curl -s https://api2.infiniteocean.io/v2/receipt/999999999999 \
  -H "X-Ocean-Key: not-a-real-key-…"
# 404 {"ok":false,"error":"not found","code":"NOT_FOUND"}

A receipt body carries only seq, state and node — no route, key or payload.

A receipt only exists on the node that accepted the write

This is the trap. The receipt is read out of the accepting node's local table. Ask any other node and you get a 404 that is indistinguishable from "that seq never existed". The same seq, queried against all five nodes at the same moment:

de   -> 404 {"ok":false,"error":"not found","code":"NOT_FOUND"}
fi   -> 404 {"ok":false,"error":"not found","code":"NOT_FOUND"}
ash  -> 404 {"ok":false,"error":"not found","code":"NOT_FOUND"}
hil  -> 200 {"ok":true,"seq":1787577342969761,"state":"durable","node":"hil"}
sin  -> 404 {"ok":false,"error":"not found","code":"NOT_FOUND"}

api2.infiniteocean.io is load-balanced across those nodes and steers by geography, so a plain curl poll will usually — but not always — reach the same node twice. Practical guidance:

Receipt errors

StatusBodyWhen
404{"ok":false,"error":"not found","code":"NOT_FOUND"}This node has no record of that seq. Four causes, indistinguishable from the response: the write settled and the receipt retired (most common), the write was accepted by a different node, the seq never existed, or the key you sent is not a key this server knows.
400{"ok":false,"error":"path does not name a valid resource","code":"BAD_PATH"}The path segment is not a decimal integer, e.g. /v2/receipt/abc.
401{"ok":false,"error":"X-Ocean-Key required","code":"KEY_REQUIRED"}No key header.
400{"ok":false,"error":"unknown query parameter \"x\" — nothing was applied","code":"UNKNOWN_PARAM"}This endpoint takes no query parameters.

GET /v2/routes

Lists the routes your key can reach, split into routes you own and routes shared with you.

curl -s https://api2.infiniteocean.io/v2/routes -H "X-Ocean-Key: $OCEAN_KEY"

Real response, abridged in the middle of mine only:

{"ok":true,
 "mine":[
   {"route":"'ft-quoted-probe'","op":"read-write","exp":0,"at":1787039643906},
   {"route":"/_docs_probe/notes","op":"read-write","exp":0,"at":1787592109033},
   {"route":"_docs_probe","op":"read-write","exp":0,"at":1787592330849},
   … 42 more …
   {"route":"_docs_probe/other","op":"read-write","exp":0,"at":1787593747149}],
 "shared":[
   {"route":"_docs_probe/access-share-exp","op":"read-write","exp":1000000000000,"at":1787593187991},
   {"route":"_docs_probe/access-share-ro","op":"read","exp":0,"at":1787593178339},
   {"route":"_docs_probe/access-share-rw","op":"read-write","exp":0,"at":1787593151217},
   {"route":"rdx1786538154","op":"read","exp":0,"at":1786538300827}],
 "mine_next":"_docs_probe/other",
 "shared_next":null,
 "mine_complete":false,
 "shared_complete":false}
FieldMeaning
mine / sharedArrays of {route, op, exp, at}. op is the grant — read or read-write. A shared route can be read-write; do not assume shared means read-only. exp is an expiry in ms, 0 meaning no expiry. at is when the grant was recorded, ms since epoch.
mine_next / shared_nextThe cursor for the next page of that side, or null. Unusable today — see below.
mine_complete / shared_completeSame meaning as complete on a list: whether the server can certify it showed you every route on that side.

This endpoint returns at most 50 routes per side and cannot be paged

The response advertises a cursor, but the endpoint rejects every query parameter, so there is no way to send one back. All measured:

curl -s "https://api2.infiniteocean.io/v2/routes?limit=3" -H "X-Ocean-Key: $OCEAN_KEY"
# 400 {"ok":false,"error":"unknown query parameter \"limit\" — nothing was applied",
#      "code":"UNKNOWN_PARAM"}

curl -s "https://api2.infiniteocean.io/v2/routes?cursor_mine=zzz" -H "X-Ocean-Key: $OCEAN_KEY"
# 400 {"ok":false,"error":"unknown query parameter \"cursor_mine\" — nothing was applied",
#      "code":"UNKNOWN_PARAM"}

mine_cursor and cursor are rejected the same way.

So if you have more than 50 owned routes you will see at most the first 50 in key order and no way to reach the rest through this endpoint. Note also that a short array does not mean the end: in the run above mine contained 46 entries — under the cap of 50 — and mine_next was still set and mine_complete was false. Do not infer "that's all of them" from mine.length; read mine_complete.

If you need the full set, track your own routes as you create them.

Routes errors

StatusBodyWhen
401{"ok":false,"error":"X-Ocean-Key required","code":"KEY_REQUIRED"}No key header.
401{"ok":false,"error":"unknown key — not authenticated","code":"UNKNOWN_KEY"}Key not recognised.
400{"ok":false,"error":"unknown query parameter \"…\" — nothing was applied","code":"UNKNOWN_PARAM"}Any query parameter.

Cross-node reads: a write is not instantly readable everywhere

The API runs on five nodes and Cloudflare steers you to the nearest one. Your write lands on one node; your next read may be served by another. Until the write has propagated, that other node returns 404 NOT_FOUND for the entity and omits it from a list.

Measured four times on one day — write on hil, then poll de once a second until it returned the payload:

run 1:  visible after 2,250 ms
run 2:  visible after 2,411 ms
run 3:  visible after   274 ms
run 4:  visible after 2,223 ms

Those are the numbers from one client, one node pair, one afternoon, and the spread within those four runs is already a factor of nine. They are not a service level. Design for retry, not for a number. What to do about it:

Caveats and things this page does not claim

Base URL https://api2.infiniteocean.io · authenticate with the X-Ocean-Key header.