Reference

Errors

Error handling on this API has a wrinkle worth knowing about before you write your client. There are two body formats, and one class of endpoint returns failures with a 200.

Two formats

Plain text — Content-Type: text/plain; charset=utf-8, with a trailing newline. Used by authentication failures, method-not-allowed, and all manifest errors:

manifest: not found

JSON — Content-Type: application/json. Used by prepare-write, cap rejections, and rate limiting:

{ "error": "redundancy 1.400 < 1.5" }

A robust client checks the content type, or simply tries to parse JSON and falls back to the raw text:

async function readError(res: Response): Promise<string> {
  const text = await res.text()
  try {
    const parsed = JSON.parse(text)
    return parsed.error ?? text
  } catch {
    return text.trim()
  }
}

The 200-with-an-error case

Check the body, not only the status

Some endpoints write a JSON error object without setting a failure status, so a failed call arrives as 200 OK with {"error": "..."} in the body. res.ok is therefore not a sufficient success check. Always inspect the parsed body for an error key. This affects operational endpoints more than the core object path, but a client that checks both is correct everywhere and costs nothing.

async function call(method: string, path: string, body?: unknown) {
  const res = await signedFetch(method, path, body)
  const text = await res.text()

  if (!res.ok) throw new TesseraError(res.status, await readErrorFrom(text))

  const parsed = text ? JSON.parse(text) : null
  if (parsed && typeof parsed === 'object' && 'error' in parsed) {
    throw new TesseraError(res.status, parsed.error) // 200, but not a success
  }
  return parsed
}

Status codes

StatusMeaningRetry?
200Success — unless the body carries an error key.—
201Credential created.—
204Success, no body. Deletes and auth/check.—
400Malformed request: bad JSON, bad hex, invalid geometry, missing slab.No — fix the request.
401Authentication failed.Only after re-signing.
402Storage cap exceeded.No — needs a cap change or a deletion.
403The resource belongs to another account.No.
404Not found for this account.No.
405Wrong HTTP method for the path.No.
409Slab still referenced by an object.After deleting the object.
429Rate limited. Account signup answers 429 signup_limit when the daily new-account cap for the source network is reached.Yes, after Retry-After; signup_limit only after up to 24 hours.
500Server-side failure.Yes, with backoff.
503A subsystem is unavailable — often no providers right now.Yes, with backoff.

Manifest error strings

Manifest errors arrive as plain text with these exact bodies. Match on them if you need to branch:

BodyStatusMeaning
manifest: not found404No such slab or object for this account.
manifest: forbidden for this account403It exists, but belongs to someone else.
manifest: invalid argument400Malformed field.
manifest: invalid erasure coding params400Geometry, duplicate provider, duplicate root, or key length.
manifest: slab still referenced by an object409Unpin blocked by a live reference.
manifest: empty account400No account resolved from the request.
manifest: object references missing slab400Register the slab first.

Authentication errors

All 401, all plain text. See Authentication § failure modes for what each one indicates and how to debug signature mismatch.

missing auth params · invalid validUntil · signature expired
invalid credential  · unknown credential · invalid signature · signature mismatch

Rate limiting

HTTP/1.1 429 Too Many Requests
Retry-After: 5

{"error":"rate limited","retryAfter":5}

Honour Retry-After. Provisioning has its own limit with a distinct body:

{ "error": "rate limited: max 5 keys per hour" }

See Limits.

A retry policy that behaves

const RETRYABLE = new Set([429, 500, 502, 503, 504])

async function withRetry<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
  let lastErr: unknown
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (err) {
      lastErr = err
      const status = (err as TesseraError).status
      if (!RETRYABLE.has(status)) throw err            // 4xx: retrying cannot help

      const retryAfter = (err as TesseraError).retryAfter
      const backoff = retryAfter
        ? retryAfter * 1000
        : Math.min(2 ** i * 250, 8000) + Math.random() * 250   // jitter matters
      await new Promise((r) => setTimeout(r, backoff))
    }
  }
  throw lastErr
}

Two things this gets right that naive retry loops do not: it refuses to retry 4xx responses, where the answer will not change, and it adds jitter, so a fleet of clients recovering from an outage does not synchronise into a thundering herd.

Errors worth special handling

SituationRight response
503 no contracted hosts available from prepare-writeBackoff and retry. Transient provider availability, not a client bug.
Shard write fails at a providerGet a replacement provider from prepare-write; do not retry the same one indefinitely.
402 storage cap exceededStop, alert an operator. Never retry on a loop.
400 altcha_required / altcha_invalid on signupFetch a fresh GET /auth/altcha-challenge, solve it, and resend with X-Altcha. Solutions are single use.
401 signature expiredRe-sign with a fresh validUntil. If it recurs, check clock skew on your host.
409 slab still referencedDelete referencing objects, then prune.
200 with an error keyTreat as the failure it is; log the endpoint, since this is the easiest failure to swallow accidentally.

Transfer errors from the CLI

Before each upload or download, tessera checks that enough storage providers are reachable and funded, and names the cause when a transfer cannot run. These are the messages and what to do:

MessageCauseFix
only X of the 30 needed hosts are reachable from your networkA firewall, VPN, proxy or sandbox blocks direct outbound TCP to the providers (siamux, on the port each one announces, usually 9984).Allow outbound TCP to the providers, or run from a network that does. Host lists carry IP addresses, so fake-IP DNS sandboxes work; HTTP(S)-only networks do not.
insufficient balanceThe technical validation is used and the balance is too low.tessera topup 1, or any amount from $0.01 with --wallet (x402). See Top-ups.
account not ready yetHost funding has not landed within the wait (2 minutes, or 45 seconds without progress).Retry in a minute; tessera account --json shows the readiness state.
not found (download)No file with that name in your index.tessera list shows the stored names.

Read-only requests retry on dropped connections (a bare EOF, a reset) before failing. Error output never includes URL query strings, which carry request signatures.