Reference

Errors

Status codes, the two different error body formats, and the one case where a failure arrives with a 200.

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 textContent-Type: text/plain; charset=utf-8, with a trailing newline. Used by authentication failures, method-not-allowed, and all manifest errors:

manifest: not found

JSONContent-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.Yes, after Retry-After.
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.
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.