Upload a file up to 4 MiB with a single authenticated PUT, read it back by id, serve it publicly at a keyless URL, and delete it.
Blobs are for bytes that do not belong in a drop payload: images, small PDFs, CSV exports, icons, JSON bundles. You upload a file with one authenticated PUT, the server gives it an id, and you read it back by that id. There is no separate upload-URL step and no signed link to fetch first — the door is the upload endpoint.
One PUT is capped at 4 MiB (4,194,304 bytes). That is a hard server limit and it is the first thing to check against your use case — see Size limit. There is no chunked or multipart upload API to work around it.
Everything on this page was run against https://api2.infiniteocean.io and the responses are the real ones. HTTP header dumps are trimmed to the headers that matter; the live responses also carry date, server, alt-svc and Cloudflare headers.
Upload a file, then read it back.
$ printf 'hello from the v2 blob door\n' > hello.txt
$ curl -s -X PUT https://api2.infiniteocean.io/v2/blob/_docs_probe/files \
-H "X-Ocean-Key: $OCEAN_KEY" \
-H "Content-Type: text/plain" \
--data-binary @hello.txt
{"url":"/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183","key":"b/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183","id":"5fb4d226afcf4e70dbaeb37872567183","bytes":28,"sha256":"65b165d16026b56c86d6bd60688bc33ce87ad256ef4b10a687357091aa7833a1"}
$ curl -s https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183 \
-H "X-Ocean-Key: $OCEAN_KEY"
hello from the v2 blob door
The id you get back will not be the one above. The server mints a random 32-hex id per upload; it is not derived from the content. Uploading the same 28 bytes a second time produced a different id and a second, independent blob. Store the id the response gives you.
The sha256 in the response is computed by the server from the bytes it stored. Compare it with your local hash and you have proof the upload is byte-identical:
$ shasum -a 256 hello.txt
65b165d16026b56c86d6bd60688bc33ce87ad256ef4b10a687357091aa7833a1 hello.txt
Stores the request body as a new blob under {route} and returns its id. Requires a key that is a principal on the route.
| Part | Required | Notes |
|---|---|---|
{route} in the path | yes | May contain slashes: /v2/blob/media/2026/covers is the route media/2026/covers. A single segment is also a route: PUT /v2/blob/mystuff uploads to route mystuff. |
X-Ocean-Key header | yes | 401 without it. |
| Request body | yes | The raw file bytes, with a Content-Length. Not a JSON wrapper, not multipart, not chunked. An empty body is a 400. |
Content-Type header | no | Stored and replayed on read. Defaults to application/octet-stream. |
?id= or X-Blob-Id | no | Name the blob yourself instead of letting the server mint an id. The header wins if both are sent (verified). |
?public=true | no | Also publish the blob at the keyless gateway. See Public blobs. |
Those two query parameters are the only ones this endpoint accepts. Every other blob endpoint accepts none. Anything else is a hard 400 — see Unknown query parameters.
Response is 201 with:
| Field | Meaning |
|---|---|
id | 32 lowercase hex characters, minted at random by the server unless you supplied one. This is what you store and what you read back with. |
url | A path, not an absolute URL: /v2/blob/{route}/{id}. Prefix it with https://api2.infiniteocean.io yourself. |
key | The internal object key, b/{route}/{id}. Informational; no endpoint takes it. |
bytes | Size of what was stored. |
sha256 | SHA-256 of the stored bytes, lowercase hex. |
Uploading from a file with -T works the same way (curl sets Content-Length from the file):
$ curl -s -X PUT https://api2.infiniteocean.io/v2/blob/_docs_probe/files \
-H "X-Ocean-Key: $OCEAN_KEY" \
-H "Content-Type: application/octet-stream" \
-T big.bin
{"url":"/v2/blob/_docs_probe/files/dceafd8831cdc4321e8155ac526dfbab","key":"b/_docs_probe/files/dceafd8831cdc4321e8155ac526dfbab","id":"dceafd8831cdc4321e8155ac526dfbab","bytes":1048576,"sha256":"f851956454a36d49a74d2214968aaba18b919bb30ada73d515b127dcd0bd630b"}
A request body larger than 4,194,304 bytes is rejected before it reaches the blob code. The response is 413 and it names the limit:
$ curl -s -X PUT https://api2.infiniteocean.io/v2/blob/_docs_probe/files \
-H "X-Ocean-Key: $OCEAN_KEY" -H "Content-Type: application/octet-stream" -T five-mib.bin
{"ok":false,"error":"body of 5242880 bytes exceeds the 4194304-byte limit — nothing was applied","code":"PAYLOAD_TOO_LARGE","limit_bytes":4194304}
The boundary is exact and was probed on both sides: a body of exactly 4,194,304 bytes returned 201; 4,194,305 bytes returned 413 PAYLOAD_TOO_LARGE. Nothing is stored on a 413.
There is no chunked upload, no multipart upload and no resume, so 4 MiB is the largest single object you can put in a blob today. If you need to store something bigger, split it yourself into blobs under 4 MiB and record the parts in a drop you write.
Transfer-Encoding: chunked is not supported, and it fails silently rather than with an error. Two observed outcomes, both wrong:
chunked small body chunked stored 29 bytes; reading it back gave 12\r\nchunked small body\r\n0\r\n\r\n, with a 201 and a sha256 of the framed bytes.400 EMPTY_BODY — the body was discarded entirely.Always send a real Content-Length. curl --data-binary @file and curl -T file both do; a streaming HTTP client that defaults to chunked does not.
The body is stored verbatim. If you send curl -F 'file=@hello.txt', the MIME envelope is what gets stored, and reading it back gives you the envelope, not the file:
$ curl -s https://api2.infiniteocean.io/v2/blob/_docs_probe/files/99999999888877776666555544443333 \
-H "X-Ocean-Key: $OCEAN_KEY"
--------------------------xFyhkdhtToXRt0vJHovGVJ
Content-Disposition: form-data; name="file"; filename="hello.txt"
Content-Type: text/plain
hello from the v2 blob door
--------------------------xFyhkdhtToXRt0vJHovGVJ--
(The boundary string is generated by curl and differs every run.) The upload also returned 201 with "bytes":227 and a Content-Type of multipart/form-data, so nothing tells you it went wrong except reading it back. Use --data-binary @file or -T file. If you are uploading from a browser form, read the File object and send it as the request body directly.
Pass ?id= (or the X-Blob-Id header) when you already have an identifier — migrating from another store, or making a re-run idempotent. Two id shapes are accepted: 32 lowercase hex characters, or a lowercase dashed UUID (8-4-4-4-12).
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?id=7fe20831-e59b-4878-82c5-53e47f13b524" \
-H "X-Ocean-Key: $OCEAN_KEY" -H "Content-Type: text/plain" \
--data-binary 'legacy uuid'
{"url":"/v2/blob/_docs_probe/files/7fe20831-e59b-4878-82c5-53e47f13b524","key":"b/_docs_probe/files/7fe20831-e59b-4878-82c5-53e47f13b524","id":"7fe20831-e59b-4878-82c5-53e47f13b524","bytes":11,"sha256":"443db030b4ce2bb6f7e0b25d48afe6cb7491f07e79122ed1c7d7e1b958f1a67e"}
Uppercase hex is rejected:
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?id=7FE20831-E59B-4878-82C5-53E47F13B524" \
-H "X-Ocean-Key: $OCEAN_KEY" --data-binary 'x'
{"ok":false,"error":"malformed blob id (need 32-hex or v1 dashed UUID) — nothing was stored","code":"BAD_BLOB_ID"}
If you send both ?id= and X-Blob-Id, the header wins — verified by sending ?id=a1a1… with X-Blob-Id: b2b2… and getting back "id":"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2".
Re-uploading the same id replaces the content. There is no conflict error and no version history for blobs. The second PUT wins:
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?id=00112233445566778899aabbccddeeff" \
-H "X-Ocean-Key: $OCEAN_KEY" -H "Content-Type: text/plain" \
--data-binary 'REPLACED body v2'
{"url":"/v2/blob/_docs_probe/files/00112233445566778899aabbccddeeff","key":"b/_docs_probe/files/00112233445566778899aabbccddeeff","id":"00112233445566778899aabbccddeeff","bytes":16,"sha256":"414aebc9732a808c042d42b1edd75f2411e80310c5410863ec1fe7530428d07d"}
$ curl -s https://api2.infiniteocean.io/v2/blob/_docs_probe/files/00112233445566778899aabbccddeeff \
-H "X-Ocean-Key: $OCEAN_KEY"
REPLACED body v2
So that uploads are listable, each successful PUT appends a drop on the same route under the key blobs/{id}. You do not have to write it and you cannot turn it off. Its payload, taken from a real /v2/list/ response:
{
"type": "text/plain",
"id": "00112233445566778899aabbccddeeff",
"_blob": {
"id": "00112233445566778899aabbccddeeff",
"key": "b/_docs_probe/files/00112233445566778899aabbccddeeff",
"url": "/v2/blob/_docs_probe/files/00112233445566778899aabbccddeeff",
"bytes": 16,
"type": "text/plain",
"sha256": "414aebc9732a808c042d42b1edd75f2411e80310c5410863ec1fe7530428d07d"
}
}
See Listing what you have uploaded for how to read these back, and for the two things this drop does not record.
Returns the raw bytes with the Content-Type the blob was uploaded with.
$ curl -s -D- https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183 \
-H "X-Ocean-Key: $OCEAN_KEY"
HTTP/2 200
content-type: text/plain
content-length: 28
access-control-allow-origin: *
access-control-expose-headers: Content-Range, Location
x-content-type-options: nosniff
hello from the v2 blob door
The last path segment is always the id and everything before it is the route, so a nested route works without any encoding. An upload to /v2/blob/_docs_probe/a/b/c is read back at /v2/blob/_docs_probe/a/b/c/{id}, verified end to end. A read path with only one segment after /v2/blob/ has no id and returns 400 BAD_PATH.
A blob is readable from every node as soon as the PUT returns. Immediately after an upload we read the new id through all five origins by IP and got 200 from each — de, fi, ash, hil, sin.
Send a Range: bytes=start-end header to fetch part of a blob. The response is 206 with a Content-Range giving the total size. Content-Range is in Access-Control-Expose-Headers, so browser JavaScript can read it.
$ curl -s -D- https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183 \
-H "X-Ocean-Key: $OCEAN_KEY" -H 'Range: bytes=0-4'
HTTP/2 206
content-type: text/plain
content-length: 5
content-range: bytes 0-4/28
hello
Open-ended ranges work and are clamped to the end of the blob:
$ curl -s -D- ... -H 'Range: bytes=6-'
HTTP/2 206
content-length: 22
content-range: bytes 6-27/28
from the v2 blob door
Three range behaviours will surprise you if you assume RFC 7233:
Range: bytes=-5 returns 400 BAD_RANGE, not the last five bytes.503 TRANSIENT, not 416. On a 28-byte blob, bytes=28-30 and bytes=100-200 both return {"ok":false,"error":"blob temporarily unavailable — retry","code":"TRANSIENT"}. This is deterministic — four repeats gave 503 every time while a plain GET of the same blob returned 200 in the same run, and a past-EOF start on a 4 MiB blob behaved identically — so retrying will not help. Check the size before you seek. An end past the blob is fine: bytes=27-99 returns 206 with content-range: bytes 27-27/28.Range is a hard error, not an ignored header. Range: items=0-4 returns 400 BAD_RANGE.HEAD /v2/blob/{route}/{id} returns 405 with an empty body. To check existence or size without downloading, use a one-byte range request and read the total out of Content-Range: Range: bytes=0-0 returns 206 with content-range: bytes 0-0/28. You can also read the blobs/{id} drop, which carries bytes.
There is no ETag on the authenticated read and no conditional-request support: an If-None-Match header is ignored and the full body is returned with 200.
Removes the blob. Returns 204 with an empty body.
$ curl -s -i -X DELETE https://api2.infiniteocean.io/v2/blob/_docs_probe/files/547625151cdcc6a7bbf4eb789a0b1c68 \
-H "X-Ocean-Key: $OCEAN_KEY"
HTTP/2 204
$ curl -s https://api2.infiniteocean.io/v2/blob/_docs_probe/files/547625151cdcc6a7bbf4eb789a0b1c68 \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":false,"error":"blob not found","code":"NOT_FOUND"}
Four things to know:
204, and so does deleting the same id twice. If you need to know whether a blob existed, GET it first.DELETE /v2/blob/_docs_probe/files returned 204; every blob on that route was still readable afterwards. A success status here does not mean a delete happened — check your URL has the id on it.404 on both public hosts. You do not need to un-publish before deleting.blobs/{id} drop. That metadata drop stays on the route and keeps appearing in /v2/list/ after the bytes are gone — confirmed by listing the route after the delete. If your UI is built from that list, either delete the drop yourself or treat a 404 from the blob GET as "removed".A public blob is served from a second, keyless URL under /pub/. Publishing is explicit and per blob: a blob you have not published returns 404 at the public URL, with the same body as an id that never existed, so the gateway does not reveal which private ids are real.
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?public=true" \
-H "X-Ocean-Key: $OCEAN_KEY" -H "Content-Type: application/json" \
--data-binary '{"logo":"v2","n":1}'
{"url":"/v2/blob/_docs_probe/files/547625151cdcc6a7bbf4eb789a0b1c68","key":"b/_docs_probe/files/547625151cdcc6a7bbf4eb789a0b1c68","id":"547625151cdcc6a7bbf4eb789a0b1c68","bytes":19,"sha256":"79065239ab64d3ee5829c798e340a47c8f55257882f69771a3b64c24356e951e"}
The response is identical in shape to a private upload — it carries no flag saying the blob is public. You know it is public because you asked for it.
?public=true combines with ?id=:
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?id=abcdef0123456789abcdef0123456789&public=true" \
-H "X-Ocean-Key: $OCEAN_KEY" -H "Content-Type: text/css" \
--data-binary 'body{color:#111}'
{"url":"/v2/blob/_docs_probe/files/abcdef0123456789abcdef0123456789","key":"b/_docs_probe/files/abcdef0123456789abcdef0123456789","id":"abcdef0123456789abcdef0123456789","bytes":16,"sha256":"cc564ca3bf4f4abe12c0ddee7a867bfe198d410fa4bd3f81c03653119091ecb8"}
No key, no headers. Both api2.infiniteocean.io and view2.infiniteocean.io serve it, and all five origins answered 200 when queried by IP.
$ curl -s -D- https://api2.infiniteocean.io/pub/_docs_probe/files/547625151cdcc6a7bbf4eb789a0b1c68
HTTP/2 200
content-type: application/json
content-length: 19
access-control-allow-origin: *
access-control-expose-headers: Content-Range, Location
cache-control: public, max-age=31536000, immutable
x-content-type-options: nosniff
{"logo":"v2","n":1}
Note the path is /pub/, not /v2/blob/. The url field returned by the upload is always the private, authenticated path; build the public one yourself as https://api2.infiniteocean.io/pub/{route}/{id}.
A blob that is not public returns 404 here:
$ curl -s https://api2.infiniteocean.io/pub/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183
{"ok":false,"error":"not found","code":"NOT_FOUND"}
The response carries cache-control: public, max-age=31536000, immutable. Since ids are never reused unless you re-PUT them, that is safe for content — but it also means any browser or CDN that has cached a public blob may keep serving it after you make it private. Treat "make private" as a slow revocation, not an instant one, and use a fresh id if you need something to be unreachable immediately.
Publishes a blob that already exists.
$ curl -s -X PUT https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183/public \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":true,"route":"_docs_probe/files","id":"5fb4d226afcf4e70dbaeb37872567183","public":true}
The keyless URL then returns the bytes. Publishing an id that does not exist returns 404:
$ curl -s -X PUT https://api2.infiniteocean.io/v2/blob/_docs_probe/files/cccccccccccccccccccccccccccccccc/public \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":false,"error":"blob not found — nothing was applied","code":"NOT_FOUND"}
The endpoint takes no query parameters — any parameter is a 400 UNKNOWN_PARAM, including plausible ones like ?ttl=60.
Un-publishes: removes the public copy and leaves the private blob intact.
$ curl -s -X DELETE https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183/public \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":true,"route":"_docs_probe/files","id":"5fb4d226afcf4e70dbaeb37872567183","public":false}
$ curl -s https://api2.infiniteocean.io/pub/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183
{"ok":false,"error":"not found","code":"NOT_FOUND"}
$ curl -s https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183 \
-H "X-Ocean-Key: $OCEAN_KEY"
hello from the v2 blob door
Whatever you send as Content-Type on the PUT is stored and replayed on every read, on both the authenticated and the public path. Nothing is sniffed or corrected — a PNG uploaded as text/plain came back as content-type: text/plain.
Omit the header and the blob is stored as application/octet-stream:
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?id=11111111222233334444555555555555" \
-H "X-Ocean-Key: $OCEAN_KEY" -H 'Content-Type:' --data-binary 'no content type here'
{"url":"/v2/blob/_docs_probe/files/11111111222233334444555555555555","key":"b/_docs_probe/files/11111111222233334444555555555555","id":"11111111222233334444555555555555","bytes":20,"sha256":"2b49932fc0b3b39ce43ef0ba7f39b75fab27863e239d7b0f534ecbac08619b67"}
$ curl -s -D- ... | grep content-type
content-type: application/octet-stream
Every response also carries x-content-type-options: nosniff, so a browser will not second-guess the type you set. If you serve user-uploaded files publicly, set the type deliberately — do not pass through a client-supplied one you have not validated.
Binary is exact. A 129-byte PNG and a 1 MiB random file were both uploaded and downloaded again and compared byte-identical with cmp, with the server's sha256 matching the local one in each case:
$ curl -s -o back.png -w '%{http_code} %{content_type} %{size_download}\n' \
https://api2.infiniteocean.io/v2/blob/_docs_probe/files/653fb15d17e7263ae33620b260a55408 \
-H "X-Ocean-Key: $OCEAN_KEY"
200 image/png 129
$ cmp tiny.png back.png && echo IDENTICAL
IDENTICAL
There is no "list blobs" endpoint. Use GET /v2/list/{route} and read the blobs/ keys that every upload writes:
$ curl -s "https://api2.infiniteocean.io/v2/list/_docs_probe/files?limit=2" \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":true,"items":[
{"key":"blobs/00112233445566778899aabbccddeeff","ts":1787593183490,
"value":{"type":"text/plain","id":"00112233445566778899aabbccddeeff",
"_blob":{"id":"00112233445566778899aabbccddeeff","key":"b/_docs_probe/files/00112233445566778899aabbccddeeff",
"url":"/v2/blob/_docs_probe/files/00112233445566778899aabbccddeeff","bytes":16,"type":"text/plain",
"sha256":"414aebc9732a808c042d42b1edd75f2411e80310c5410863ec1fe7530428d07d"}}},
{"key":"blobs/11111111222233334444555555555555","ts":1787593189442,
"value":{"type":"application/octet-stream","id":"11111111222233334444555555555555",
"_blob":{"id":"11111111222233334444555555555555","key":"b/_docs_probe/files/11111111222233334444555555555555",
"url":"/v2/blob/_docs_probe/files/11111111222233334444555555555555","bytes":20,"type":"application/octet-stream",
"sha256":"2b49932fc0b3b39ce43ef0ba7f39b75fab27863e239d7b0f534ecbac08619b67"}}}
],"returned":2,"complete":false,"index":true,"next_cursor":"blobs/11111111222233334444555555555555"}
Items come back ordered by key, and next_cursor feeds the ?cursor= parameter for the next page. Your own drops on the same route are interleaved with the blobs/ ones.
Two limits of that drop, both of which bite in practice:
So write your own drop for anything you need to query on, and reference the blob by id:
$ 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/files","key":"doc-42",
"payload":{"filename":"report.pdf",
"blob_id":"5fb4d226afcf4e70dbaeb37872567183",
"uploaded_by":"docs-probe"}}'
{"ok":true,"seq":1787577342969576,"offset":0,"len":166,"ts":1787593855218,"state":"accepted","rule_created":false,"drop_id":"1787577342969576","route":"_docs_probe/files","key":"doc-42"}
$ curl -s https://api2.infiniteocean.io/v2/entity/_docs_probe/files/doc-42 -H "X-Ocean-Key: $OCEAN_KEY"
{"filename":"report.pdf","blob_id":"5fb4d226afcf4e70dbaeb37872567183","uploaded_by":"docs-probe"}
Note that you cannot read the auto-written metadata drop through /v2/entity/: its key contains a slash (blobs/{id}), and /v2/entity/ treats the last path segment as the key, so /v2/entity/_docs_probe/files/blobs/{id} is parsed as a different route and returns 403 FORBIDDEN. Reach those drops through /v2/list/.
Blob permissions are the route's permissions — the same access rules that govern drops on that route. A key that is not a principal on the route gets 403 on every blob operation:
$ curl -s https://api2.infiniteocean.io/v2/blob/cc/log/ffffffffffffffffffffffffffffffff \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":false,"error":"authenticated key is not a principal on this route","code":"NOT_PRINCIPAL"}
The first upload to a route that does not exist yet creates it and claims it for your key. Reading the route's access afterwards showed your key as owner, enforced: true and no shares:
$ curl -s -X PUT https://api2.infiniteocean.io/v2/blob/_docs_probe/fresh-1787593351 \
-H "X-Ocean-Key: $OCEAN_KEY" -H "Content-Type: text/plain" \
--data-binary 'first upload claims the route'
{"url":"/v2/blob/_docs_probe/fresh-1787593351/fc099f4fa761a4ab1e063c16807f3870", ... }
$ curl -s "https://api2.infiniteocean.io/v2/access/_docs_probe%2Ffresh-1787593351" \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":true,"route":"_docs_probe/fresh-1787593351","owner":"9a92e0aed98f","owner_handle":"…","enforced":true,"shares":[]}
Errors are JSON with ok, error and a machine-readable code. Every one of these was produced against the live door.
| Status | code | When |
|---|---|---|
| 400 | EMPTY_BODY | PUT with a zero-length body — or a chunked body the server could not read. Nothing was stored. |
| 400 | BAD_BLOB_ID | ?id= or X-Blob-Id is not 32 lowercase hex or a lowercase dashed UUID. Nothing was stored. |
| 400 | BAD_RANGE | The Range header is not bytes=start-end or bytes=start-. Suffix ranges land here too. |
| 400 | BAD_PATH | A read or delete path that does not name a route and an id — e.g. GET /v2/blob/onlyone. |
| 400 | UNKNOWN_PARAM | Any query parameter the endpoint does not define. |
| 401 | KEY_REQUIRED | No X-Ocean-Key header on a /v2/blob/ request. |
| 401 | UNKNOWN_KEY | The key is not recognised. |
| 403 | NOT_PRINCIPAL | Valid key, but not a principal on that route. |
| 404 | NOT_FOUND | No blob with that id (authenticated GET), or the blob is not public (/pub/), or the id does not exist on the publish door. Also returned for a method/path with no endpoint, e.g. POST /v2/blob/{route}. |
| 405 | — | HEAD on a blob. Empty body. |
| 413 | PAYLOAD_TOO_LARGE | Request body over 4,194,304 bytes. The response carries limit_bytes. Nothing was stored. |
| 503 | TRANSIENT | A range whose start is at or past the end of the blob. Also the genuine "storage unavailable, retry" case — the two are indistinguishable from the response, so validate your offsets. |
Every /v2/ endpoint 400s on a parameter it does not define. The blob upload defines exactly two — id and public — and every other blob endpoint defines none. So a plausible-looking parameter fails loudly rather than being silently dropped:
$ curl -s "https://api2.infiniteocean.io/v2/blob/_docs_probe/files/5fb4d226afcf4e70dbaeb37872567183?download=true" \
-H "X-Ocean-Key: $OCEAN_KEY"
{"ok":false,"error":"unknown query parameter \"download\" — nothing was applied","code":"UNKNOWN_PARAM"}
$ curl -s -X PUT "https://api2.infiniteocean.io/v2/blob/_docs_probe/files?filename=x.txt" \
-H "X-Ocean-Key: $OCEAN_KEY" --data-binary 'x'
{"ok":false,"error":"unknown query parameter \"filename\" — nothing was applied","code":"UNKNOWN_PARAM"}
Listed because a developer coming from S3 or from another file API will look for them.
/v2/blob/upload-url and no /v2/upload-url; both return 404 {"ok":false,"error":"no /v2 endpoint matches this method and path","code":"NOT_FOUND"}. Upload straight to PUT /v2/blob/{route} with your key.POST to the blob door. POST /v2/blob/{route} returns 404; the method is PUT.HEAD, no suffix ranges, no ETag / If-None-Match on the authenticated path.https://api2.infiniteocean.io · authenticate with the X-Ocean-Key header.