Reference

HTTP API

Every endpoint ZFSRecv exposes: authentication, pool setup, listing, uploads and downloads, destroy, holds, rename/rollback/bookmarks, properties, and resumable transfers.

19 min read

The entire client-facing surface is plain, synchronous HTTPS. There’s no agent, no SDK, and no framing on top of a zfs send stream — just stock zfs send | curl -T - or curl | zfs receive talking to us directly. If you haven’t already, the quickstart walks through your first send end to end; this page is the full reference.

One URL convention holds across every endpoint: the dataset or snapshot name is a path segment, never a query parameter — ZFS names already contain /, so query-encoding one would just add friction. A request to a real path with the wrong HTTP method gets an automatic 405 with an Allow header, before anything else runs.

Authentication

Every endpoint except /healthz requires a token, as either:

  • Authorization: Bearer YOUR-TOKEN-HERE, or
  • ?token=YOUR-TOKEN-HERE

(the header wins if you somehow send both). The token is the one shown once when you create a pool from your account page — copy it immediately, it isn’t displayed again.

  • 401 — missing, malformed, unknown token, a pool whose traffic landed on the wrong region’s endpoint, or a pool on a plan this endpoint doesn’t serve.
  • 429 (with Retry-After) — too many failed authentication attempts from your address (10 within a minute). Everything from that address is refused until the window passes, so a broken, retrying client can’t hammer the login path.
  • 503 — your token couldn’t be checked right now (a transient problem on our side). This is deliberately a different code from 401 so your tooling doesn’t conclude the token itself is bad and give up — back off and retry.

Token checks are cached briefly, so revoking a pool from the account page can take up to a minute to take effect everywhere.

Request IDs

Every response carries an X-Request-Id header — echoing one you sent, if it looks like [A-Za-z0-9._-]{1,64}, otherwise a random one we generate. Send your own if you want to correlate it with your own logs. If you ever need to report a problem, this id is the fastest way for us to find the matching entry server-side — internal failures are only ever reported to you as a generic message plus this id, never a raw stack trace.

Endpoints

Endpoint Purpose zfs equivalent
PUT /v1/pool_create create your account’s pool zpool create [-o compatibility=]
DELETE /v1/pool_destroy destroy your pool and everything in it zpool destroy
PUT /v1/pool_upgrade enable more pool features zpool upgrade
GET /v1/pool_get read the pool’s own properties zpool get -p [all|prop,...]
GET /v1/list[/{fs...}] list filesystems, volumes and snapshots (and, with type=, bookmarks) zfs list [-t ...]
GET /v1/detailed[/{fs...}] list with GUIDs, origins, space, resume tokens zfs list -o space,guid,origin
PUT /v1/recv/{target...} upload a send stream zfs receive
GET /v1/send/{snapshot...} download a send stream zfs send
DELETE /v1/destroy/{target...} destroy a dataset, bookmark, or snapshot(s) zfs destroy [-r]
GET /v1/token/{fs...} fetch a partial receive’s resume token zfs get receive_resume_token
DELETE /v1/abort/{fs...} discard partial receive state zfs receive -A
PUT /v1/hold/{tag}/{snapshot...} place a user hold zfs hold [-r]
DELETE /v1/release/{tag}/{snapshot...} remove a user hold zfs release [-r]
GET /v1/holds/{snapshot...} list user holds zfs holds [-r]
PUT /v1/rename/{src...} rename a filesystem or snapshot zfs rename [-r]
PUT /v1/rollback/{snapshot...} roll a filesystem back to a snapshot zfs rollback [-r]
PUT /v1/bookmark/{source...} create a bookmark zfs bookmark
GET /v1/props/{target...} read every property zfs get -p all
PUT /v1/props/{target...} set properties zfs set
PUT /v1/inherit/{target...} clear a property’s local value zfs inherit [-rS]
GET /healthz health check, unauthenticated —

Your account has no pool until PUT /v1/pool_create makes one. Until then, every other endpoint answers 404:

{"ok":false,"errno":2,"error":"the pool has not been created yet (pool_create)","no_pool":true}

Unless noted otherwise, success is 200 with {"ok":true, ...}, and a rejected request is 502 with {"ok":false,"errno":E,"error":"..."} — errno is a POSIX errno from the underlying ZFS call wherever one applies (EEXIST, EBUSY, ENOENT, …).

Managing your pool

PUT /v1/pool_create?token=YOUR-TOKEN-HERE&compat=SET

compat is optional. Creates your account’s pool, sized to your plan — this is the one call that has to happen before anything else works. Fails (502, EEXIST) if a pool already exists for your account.

compat pins the pool to an older OpenZFS release’s feature set, from the same compatibility database zpool create -o compatibility= draws on — e.g. openzfs-2.1-linux, openzfs-2.2, freebsd-11.0, several comma-separated (only features common to all of them), legacy (no features at all), or the default, off (every feature we support). An upload whose stream needs a feature outside that set is refused, so pin this to whatever release you’ll actually be restoring to, if that matters to you — it also limits what pool_upgrade can later turn on.

{"ok":true,"pool":{"compatibility":"openzfs-2.1-linux","features":{
  "async_destroy":"enabled","lz4_compress":"active","block_cloning":"disabled"}}}

Every feature we support appears, reported the same way zpool get feature@... does (disabled, enabled or active). 502 otherwise — EEXIST if a pool already exists, or an unrecognized compat set name.

DELETE /v1/pool_destroy?token=YOUR-TOKEN-HERE&confirm=GUID

Destroys your pool — every dataset, snapshot and bookmark in it, holds included — and frees its space on our side. confirm is required and must be the pool’s own GUID, which you read first with GET /v1/pool_get?property=guid: a deliberate extra lookup, so a stale or replayed request can never destroy a pool created after it. A missing or non-numeric confirm is 400; any other number is 502 (“confirm does not match the pool’s guid”), and nothing is destroyed.

This is irreversible.

{"ok":true}

Afterwards your account is exactly as it was before pool_create — every other endpoint answers 404 again — until PUT /v1/pool_create makes a new pool, optionally with a different compat=. 404 if there was no pool to destroy.

PUT /v1/pool_upgrade?token=YOUR-TOKEN-HERE&features=a,b&compat=SET

Both optional. Turns on pool features, same idea as zpool upgrade:

  • features enables exactly those (and whatever they depend on) — each must already be allowed by the pool’s compatibility set, or it’s refused.
  • without features, every feature your current compatibility set allows is turned on.
  • compat changes the compatibility set itself first — e.g. compat=openzfs-2.2 alone means “allow whatever 2.2 supports,” then enables it.

Features can never be disabled again. Narrowing compat below what’s already enabled only warns — it doesn’t undo anything — and a restore that uses an enabled feature is no longer receivable by an older release.

{"ok":true,"pool":{"compatibility":"...","features":{...},"enabled":["block_cloning"]}}

Same shape as pool_create’s response, plus enabled: the features this specific call turned on.

GET /v1/pool_get?token=YOUR-TOKEN-HERE&property=a,b

property is optional (default: every property). Reads the pool’s own properties — zpool get -p, shaped like zpool get -j:

{"ok":true,"properties":{
  "size":{"value":2199023255552,"source":{"type":"NONE","data":"-"}},
  "health":{"value":"ONLINE","source":{"type":"NONE","data":"-"}},
  "compatibility":{"value":"openzfs-2.1-linux","source":{"type":"LOCAL","data":"-"}},
  "feature@block_cloning":{"value":"disabled","source":{"type":"LOCAL","data":"-"}}
}}

size/free/allocated/capacity and friends reflect your plan’s storage, not a physical disk. A feature@NAME entry appears for every feature we support. Server-internal details (our internal pool name, altroot, cachefile) are left out. An unknown or excluded property is 502; a malformed property list is 400.

Listing datasets and snapshots

GET /v1/list?token=YOUR-TOKEN-HERE                 (whole pool)
GET /v1/list/{fs...}?token=YOUR-TOKEN-HERE         (filtered to one dataset tree)

Optionally add &type=filesystem,volume,snapshot,bookmark (short forms fs/vol/snap work too, or all), comma-separated. The default lists filesystems, volumes and snapshots — bookmarks only appear when you ask for them. &depth=N (zfs list -d) limits how far down the tree to go: at most N levels below fs (or below the pool’s top, for the whole-pool form), where a snapshot or bookmark counts as one level below its own dataset — so depth=1 of a filesystem returns just it, its snapshots and bookmarks, and its immediate children. A negative or non-numeric depth is 400. Everything comes back in the same order plain zfs list does: hierarchically by pathname, each dataset immediately followed by its own snapshots, then its bookmarks (both in creation order).

{"ok":true,"datasets":["backups/fs1","backups/fs1@s1","backups/fs1@s2",...]}

GET /v1/detailed[/{fs...}] (same type and depth filters) is the same listing with per-entry detail instead of bare names — each entry’s type, GUID, creation txg, space accounting, a volume’s size, a clone’s origin, and (for a filesystem or volume with a receive in progress or interrupted) its resume token:

{"ok":true,"datasets":[
  {"name":"backups/fs1","type":"filesystem","guid":123,"createtxg":9001,"used":N,
   "available":N,"referenced":N,"usedbysnapshots":N,"usedbydataset":N,
   "usedbyrefreservation":N,"usedbychildren":N},
  {"name":"backups/fs1@s1","type":"snapshot","guid":456,"createtxg":9002,"used":N,
   "available":N,"referenced":N},
  {"name":"backups/vol1","type":"volume","guid":789,"createtxg":9005,"volsize":N,
   "volblocksize":N,"used":N,"available":N,"referenced":N,"usedbysnapshots":N,
   "usedbydataset":N,"usedbyrefreservation":N,"usedbychildren":N},
  {"name":"backups/fs1#mark","type":"bookmark","guid":456,"createtxg":9002}
]}

type/guid/createtxg/used/available/referenced are present on every dataset or snapshot entry; a bookmark only carries type/guid/createtxg. A volume also carries volsize/volblocksize in bytes. used/available/referenced mirror zfs list -o space’s USED/AVAIL/REFER columns. The four usedby* fields only apply to filesystems and volumes, not snapshots, so they’re simply omitted on a snapshot entry rather than reported as zero. origin (the snapshot a clone was created from) appears only on a clone’s entry.

Uploading a stream

PUT /v1/recv/{target...}?token=YOUR-TOKEN-HERE

Optional query parameters:

Parameter Default zfs receive equivalent
force=true|false false -F
resumable=true|false true -s

The request body is the zfs send stream, byte for byte — no JSON, no framing — so a stock pipeline talks to the endpoint directly:

shell
# zfs send -Rw backups/fs1@s1 | \
    curl -T - "https://us-sw-01.zfsrecv.com/v1/recv/backups/fs1@s1?token=YOUR-TOKEN-HERE"

Every stream zfs send can produce is accepted: a plain stream, an incremental, a full replication package (-R, -I, -p, -b, -h), raw or not, from a sender of either byte order.

{"ok":true,"bytes":123456789,"received":["backups/fs1@s1"]}

received lists, in order, every snapshot that actually landed — on a failure partway through a package, whatever got in before the error (a package upload isn’t atomic, same as in plain ZFS). A failed receive returns 502 with errno/error, the same received list, a resume_token only when the failure left resumable state, and warnings only when something non-fatal couldn’t be applied (e.g. a rename that didn’t carry over):

{"ok":false,"errno":5,"error":"...","resume_token":"1-...","received":["backups/fs1@s1"]}

The target may omit the @snapshot suffix for a plain substream (the name is then taken from the stream itself); a recursive (-R-style) package target must never include @.

force behaves differently depending on what you send. For a plain stream, an incremental rolls the target back to its source snapshot first (discarding anything newer); a full stream may only replace an existing filesystem that has no snapshots — refused otherwise, as zfs receive -F does. For a replication stream (zfs send -R -i/-I), force additionally lets ZFSRecv destroy snapshots and filesystems that no longer exist at your source — without it, everything else in the package (renames, promotions, property updates) still applies, just not destruction.

resumable=false (zfs receive without -s) means a failed upload leaves nothing behind and returns no resume_token. The default, true, is what makes resuming an interrupted upload possible.

Encrypted datasets must be sent raw (-w) — we never hold your key, so we can’t encrypt a plaintext stream on your behalf. A clone is received as a clone if its origin snapshot is already stored somewhere in the pool (searched by GUID, not by name); otherwise the receive fails. Uploads may be capped in size by server policy; an oversized body fails exactly like any other interrupted one.

A few zfs receive options have no equivalent here, by design:

Option Why not
-o / -x (override or exclude properties) A backup should hold what you sent; a restore hands back exactly what was received.
-d / -e (derive the target name from the stream) The target is always the URL path.
-u (don’t mount) Nothing on our side is ever mounted.
-c (corrective/healing receive) Repairing a pool from a stream isn’t something this API does.
-n (dry run) A dry run would still mean uploading the whole stream — there’s nothing to gain over HTTP.

Downloading a stream

GET /v1/send/{snapshot...}?token=YOUR-TOKEN-HERE

Optional query parameters:

Parameter Default zfs send equivalent
from=FS@SNAP (or FS#BOOKMARK) full send -i
raw=true|false true -w
compressed=true|false true -c
largeblock=true|false true -L
embed=true|false true -e (implied by raw)
holds=true|false false -h
props=true|false false -p
backup=true|false false -b (received property values only)
recursive=true|false false -R
intermediates=true|false false -I (needs from, unless recursive)
estimate=true|false false -nP — report the size instead of sending

The response body is the raw restore stream, byte for byte, so it pipes straight into zfs receive:

curl "https://us-sw-01.zfsrecv.com/v1/send/backups/fs1@s1?token=YOUR-TOKEN-HERE" | zfs receive otherpool/fs1

Response is 200, Content-Type: application/octet-stream, chunked (the length isn’t known upfront), with an X-Zbackupd-Status: ok trailer once the body completes cleanly. If the send fails before any bytes went out (e.g. the snapshot doesn’t exist), you get an ordinary 502 JSON error instead. If it fails after streaming has already started, a response that already committed to 200 can’t be downgraded — the only reliable signal is the connection closing before a clean end (best-effort, there’s also an X-Zbackupd-Status: error: ... trailer).

raw and compressed default on — a restore comes back exactly as stored. If the dataset was received raw (encrypted), the restore arrives still encrypted and needs zfs load-key on your side; requesting raw=false for an encrypted dataset is refused (502, EINVAL), since we never hold the key to decrypt it for you. For unencrypted data, raw just means “keep it compressed.” Pass raw=false&compressed=false for a plain stream.

recursive=true restores a whole replication package — the snapshot’s filesystem and every descendant, each with its own snapshots — exactly like zfs send -R, unpacked on the other end by zfs receive. holds=true includes each snapshot’s user holds (re-created by zfs receive); props=true includes every property value currently set; backup=true includes only the values originally received, even if something local has since overridden them. Any of the three turns even a single-snapshot send into a small package. With recursive, intermediates, holds, props or backup, from must name a snapshot of the same filesystem as snapshot, and applies to every descendant by its last component — same as zfs send -R -i.

Resuming an interrupted restore (zfs send -t): GET /v1/send?token=YOUR-TOKEN-HERE&resume=RESUME_TOKEN (no snapshot in the path) resumes the stream described by a resume token — either one you got back from your receiving side (zfs get receive_resume_token), or one we handed back from a previous PUT /v1/recv. The token fully determines the snapshot, source, offset and flags, so no other parameter may be given except estimate.

Estimating a restore’s size without downloading it: add estimate=true to any of the above to get 200 with JSON instead of the stream itself:

{"ok":true,"estimate":{"substreams":[{"snapshot":"backups/fs1@s1","from":null,"bytes":N}],"bytes":12345}}

One entry per substream the real transfer would contain, sized the same way zfs send -nP estimates — from block accounting, not a dry run of the actual send.

Destroying a dataset, bookmark or snapshot(s)

DELETE /v1/destroy/{target...}?token=YOUR-TOKEN-HERE&recursive=true

recursive is optional (default false). target can be:

  • a dataset (filesystem or volume);
  • a bookmark — percent-encode its # as %23: /v1/destroy/backups/fs1%23mark;
  • one snapshot (fs@snap), several (fs@a,b,c), or a creation-order range: fs@a%c destroys a through c inclusive; leave either end off for “from the oldest” (fs@%c), “to the newest” (fs@a%), or both for every snapshot of fs (fs@%). % must itself be percent-encoded as %25 — /v1/destroy/backups/fs1@a%25c — and a list and a range combine: fs@a%c,e.

With a list or range, every matching snapshot is destroyed all together or not at all: if any one of them refuses (held: EBUSY; a clone origin: EEXIST), nothing is destroyed, and the error names the one that failed. A name or range end a filesystem doesn’t have is simply skipped, same as zfs destroy; if nothing matched anywhere, it’s ENOENT. The one exception is a single, non-recursive fs@snap that’s already gone — that still succeeds.

For a dataset, recursive works as zfs destroy’s own safety rules do: refuses if it has children or its own snapshots, unless set, in which case descendants are destroyed leaf-to-root before the target itself. recursive with a snapshot, list or range instead also reaches the same-named snapshots of every descendant; recursive with a bookmark is refused, same as zfs destroy. A snapshot that’s a clone origin is never chased and destroyed along with its clones; it just fails EEXIST, same as plain zfs destroy -r in that situation.

This is irreversible.

{"ok":false,"errno":17,"error":"dataset has children"}

Most common errnos: EEXIST (children, or a clone origin, without recursive) and EBUSY (the dataset still owns snapshots, or something holds it).

Resuming an interrupted upload

GET /v1/token/{fs...}?token=YOUR-TOKEN-HERE

Returns the resume token for a partial receive on fs, if one exists, to pass straight to zfs send -t:

{"ok":true,"resume_token":"1-..."}

502 with {"ok":false,"error":"..."} if there’s no resume state to report.

To discard partial receive state instead of resuming it (equivalent to zfs receive -A):

DELETE /v1/abort/{fs...}?token=YOUR-TOKEN-HERE

(PUT /v1/abort/{fs...} and DELETE /v1/recv/{fs...} are accepted as the same operation, whichever reads more naturally against your tooling.) This is irreversible — it clears whatever was resumable. 502 if there was nothing to abort.

Holds

PUT /v1/hold/{tag}/{snapshot...}?token=YOUR-TOKEN-HERE&recursive=true

Places a user hold named tag on snapshot — tag comes before snapshot in the URL, same order as zfs hold <tag> <snap>. While held, the snapshot refuses destroy (EBUSY) until released. recursive (optional, default false) also places the same tag on every descendant that has a same-named snapshot. As with zfs hold, a tag starting with . is reserved, and holding the same tag twice on the same snapshot is EEXIST. Every hold placed this way is permanent — there’s no equivalent of zfs hold -t.

DELETE /v1/release/{tag}/{snapshot...}?token=YOUR-TOKEN-HERE&recursive=true

Removes a hold previously placed by hold. (PUT /v1/release/... and DELETE /v1/hold/... are accepted as the same operation.) recursive releases the same tag from every descendant that has it; a descendant missing the snapshot or the tag is simply skipped — unless nothing at all had the tag, in which case the whole request fails (ENOENT if snapshot itself doesn’t exist, otherwise “no such tag”).

GET /v1/holds/{snapshot...}?token=YOUR-TOKEN-HERE&recursive=true

Lists holds on snapshot (and, with recursive, on every descendant that has a matching one):

{"ok":true,"holds":[{"name":"backups/fs1@s1","tag":"mytag","timestamp":1700000000}]}

One entry per (snapshot, tag) pair — a snapshot with several holds appears several times. Unlike hold/release, no matching snapshot or no holds at all is never an error here; the array is just empty, the same way list/detailed report an empty set rather than failing.

Renaming

PUT /v1/rename/{src...}?token=YOUR-TOKEN-HERE&to=DST&recursive=true

recursive is optional (default false). Renames a filesystem — and everything below it — or a snapshot, to the full name to. src and to must both be snapshots, or both not — mixing the two is 400. recursive (zfs rename -r) renames the same-named snapshot of every descendant too, so it only applies to snapshot renames. Nothing is ever mounted here, so there’s no -u/-f equivalent, and there’s no -p: the destination’s parent must already exist.

{"ok":true}

502 with errno/error otherwise — EEXIST when to already exists, ENOENT when src or the destination’s parent doesn’t.

Rolling back

PUT /v1/rollback/{snapshot...}?token=YOUR-TOKEN-HERE&destroy_newer=true

Rolls snapshot’s filesystem back to it, discarding every change made since — same as zfs rollback. Refuses while anything newer (a snapshot or bookmark) exists on that filesystem, unless destroy_newer=true (zfs rollback -r) destroys them first. A newer snapshot that’s a clone origin or currently held still blocks it either way.

This is irreversible.

{"ok":true,"target":"zb_alice/backups/fs1@s1"}

target names the snapshot rolled back to — as a full pool name including our internal prefix, unlike every name you pass in, which is relative to your account. 502 with EEXIST (“more recent snapshots or bookmarks exist”) if you didn’t set destroy_newer.

Bookmarks

PUT /v1/bookmark/{source...}?token=YOUR-TOKEN-HERE&bookmark=FS%23NAME

Creates a bookmark of snapshot source (or a copy of bookmark source, if that’s what it names). # must be percent-encoded as %23, in the path and in bookmark alike — a literal # starts a URL’s fragment, which never reaches the server. bookmark may be just %23NAME, meaning a bookmark on source’s own filesystem, same as plain zfs bookmark.

{"ok":true}

502 with errno/error otherwise — EEXIST if that bookmark already exists.

Properties

GET /v1/props/{target...}?token=YOUR-TOKEN-HERE

Every property of a filesystem, volume or snapshot, shaped like zfs get -j:

{"ok":true,"properties":{
  "compression":{"value":"lz4","source":{"type":"LOCAL","data":"-"}},
  "quota":{"value":10737418240,"source":{"type":"NONE","data":"-"}},
  "user:team":{"value":"ops","source":{"type":"INHERITED","data":"backups"}}
}}

Index properties come back by name ("lz4", "on"); everything else is a JSON number (bytes, seconds since the epoch, 100 meaning a 1.00x ratio) or string. Every dataset name — including source.data for an inherited property — is relative to your account, same as everywhere else in this API. source.type is LOCAL (set directly), RECEIVED (arrived on a receive), INHERITED (data names where it’s inherited from), DEFAULT (ZFS’s own built-in) or NONE (a statistic, not a settable property). User properties (module:name) are included; properties ZFS only derives from actually being mounted (mounted, an unset mountpoint, …) are left out, since nothing here is ever mounted.

PUT /v1/props/{target...}?token=YOUR-TOKEN-HERE

with a JSON body naming the properties to set, leaving everything else alone:

{"compression":"lz4","quota":"10G","user:team":"ops"}

Values are written exactly as zfs set takes them — strings like "lz4", "10G", "1.5T", "none" (clears a quota or reservation), or non-negative integers. The whole request is validated before anything changes: one bad value (an unknown, read-only or creation-only property; an invalid value; "quota":0 instead of "none") fails the entire call — nothing partially applies. A volume’s volsize can’t be changed this way (volumes aren’t resized here). Most properties have no effect on a never-mounted pool like this one, but are stored, returned by GET /v1/props, and included in a restore with props=true/recursive=true. To undo a value you’ve set, see inheriting a property below.

{"ok":false,"errno":22,"error":"property 'quota' must not be 0 (use none to disable it)","failed":{"quota":22}}

error describes the first problem found; failed names every property that was rejected. A body that isn’t a flat JSON object of valid property names to strings or integers is 400; one over 1 MiB is 413.

Inheriting a property

PUT /v1/inherit/{target...}?token=YOUR-TOKEN-HERE&property=NAME&recursive=true&received=true

Clears property’s locally-set value on a filesystem or snapshot, same as zfs inherit — it reverts to whatever its parent has, or ZFS’s own default if nothing does. received=true (zfs inherit -S) instead reverts to whatever the last receive set, and is the only way to clear quota/reservation/refquota/refreservation — those fail EINVAL without it, since they have no ordinary inherited value to fall back to. recursive=true (zfs inherit -r) does the same on every descendant and snapshot too, silently skipping ones the property doesn’t apply to. A read-only property fails EINVAL.

{"ok":true}

502 with errno/error otherwise.

Naming rules

Every dataset, snapshot, bookmark or hold tag you reference is validated before it reaches storage, following ZFS’s own naming rules: path components of letters, digits, _, ., :, or -, never empty or ./.., at most 200 bytes total, with exactly one @ (snapshot) or # (bookmark) where one is expected. Hold tags accept any printable character but can’t start with .. Anything that fails this is 400, before any handler runs — and a literal .. or // in the URL path itself never even reaches that check, it’s redirected to the cleaned path first.

Boolean query parameters (force, recursive, estimate, and the rest) are just as strict: only 1/t/T/true/True/TRUE or 0/f/F/false/False/FALSE are accepted. A parameter you don’t send takes its documented default, but a typo like recursive=yes is 400 — never silently read as false.

Errors

Status Meaning
200 success — {"ok":true, ...}, or for /v1/send, the raw stream itself
400 a name failed validation (see naming rules above)
401 missing/invalid token, or a pool on a plan this endpoint doesn’t serve
404 your account has no pool yet — call PUT /v1/pool_create first, see Endpoints
405 wrong HTTP method for an otherwise-real path (Allow header names the right one)
413 request body too large (PUT /v1/props’s JSON body over 1 MiB)
429 too many failed logins from your address — see Authentication
502 the operation itself failed — {"ok":false,"errno":E,"error":"..."}
503 couldn’t check your token right now, or your account is momentarily busy — retry, honoring Retry-After if present
507 we’re low on storage — uploads and new pools’ first use are refused until that clears

Limits

  • Concurrency: each pool is served by one worker at a time. A second request that arrives while one’s already in flight waits up to 15 seconds, then fails 503 with Retry-After rather than queue indefinitely — retry it.
  • Idle timeout: /v1/recv and /v1/send bodies abort after 60 seconds with zero bytes moving in either direction. This is a sliding deadline reset on every chunk that makes progress, so a large, slow-but-progressing transfer is never cut off — only a connection that’s gone genuinely silent is dropped.
  • Health check: GET /healthz needs no token and returns 200 {"ok":true,...} when healthy, or 503 {"ok":false,"error":"..."} if we’re out of capacity — safe to poll from your own monitoring.