Sitowise
API

Errors

One error shape everywhere, a correct status code, and a message written for a person. Nothing internal is ever sent to a client.

The envelope

Every error, without exception
{ "error": "That is not a valid wallet address." }

One field. No error codes, no nested detail object, no stack trace, no SQL, and no environment variable names. When something unexpected happens, the detail goes to the server log and the client gets a sentence it can show a user.

Match on the status code, not on the message text. Messages are written for people and can be reworded; statuses are the contract.

Status codes

StatusMeaningRetry
200Success. An empty array is a success, not an error.Not applicable
400The request was malformed: a bad address, a non-numeric id, a limit out of range, a value larger than a uint256, a body that is not a JSON object, or a transaction that exists but does not say what the caller claims.No, fix the request
401Not signed in, the session expired, or a sign-in signature that did not verify. On /api/me this is a normal answer for a visitor who has not connected. Also what an admin route returns for a wrong x-admin-key.After signing in again
403The thing exists but is not yours. Only /api/nodes/sync returns this, when the chain names a different wallet as the node’s owner.No, sign in with the owning wallet
404No such node, or a mint transaction that has not confirmed yet. Also what admin routes return when no admin key is configured, so an unconfigured admin surface does not advertise itself.Only for the unconfirmed-transaction case
409The request was well formed but the chain does not agree with it. Only /api/nodes/sync returns this.Yes, once the transaction has settled
429Rate limited. The response carries retry-after in seconds.Yes, after retry-after
500An unexpected failure. Details are in the server log, never in the response.Yes, with backoff
503The service is misconfigured or a dependency is unavailable, for example no reachable database, or no payments address configured for /api/deploy-quote to quote.Yes, with backoff

Handling them

A correct client
async function getJson<T>(url: string): Promise<T> {
  const res  = await fetch(url, {headers: {accept: "application/json"}});
  const body = await res.json().catch(() => null);

  if (!res.ok) {
    // The envelope is guaranteed on every error status.
    const message = body && typeof body === "object" && "error" in body
      ? String(body.error)
      : `Request failed with status ${res.status}`;

    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after") ?? 5);
      throw new RetryableError(message, wait);
    }
    throw new Error(message);
  }

  return body as T;
}

Retry 429, 500 and 503 with exponential backoff. Do not retry 400, 401 or 403, because nothing about the request will have changed. The two that depend on timing are 404 and 409 on /api/nodes/sync: both can mean the chain has not caught up yet, and both clear on their own once the transaction settles.

Rate limit responses

429
HTTP/1.1 429 Too Many Requests
x-ratelimit-limit:     60
x-ratelimit-remaining: 0
x-ratelimit-reset:     1756041660
retry-after:           23

{ "error": "Too many requests. Slow down and try again shortly." }

The three x-ratelimit- headers are on every answer, success or failure, so a client can slow down before it is refused. retry-after appears only on a 429. x-ratelimit-reset is unix seconds, not a duration.

Windows are one minute and fixed, not sliding, so the allowance refills all at once at x-ratelimit-reset. The limit differs by endpoint, from 120 a minute on /api/stats and /api/me down to 20 on the sign-in routes and /api/nodes/sync; the full table is on the API overview. Counters are held per server instance, which means the practical allowance can exceed the documented one. Treat the documented figure as the floor.

Withdrawal failures are contract reverts, not API errors

Withdrawing does not touch this API. It is a call your own wallet makes to the factory, so when it fails, the failure comes back from the chain as a revert and nothing on this page applies to it. There is no status code, no error field, and no endpoint to ask.

The custom errors the contract can revert with, all of which take no arguments, are listed on Factory interface. What each one means in practice, and what to do about it, is on Withdrawing, with the wider set of symptoms on Troubleshooting. A revert costs gas and changes nothing else: a failed withdrawal leaves the node’s balance exactly where it was.

When the API refuses before the chain would

One route deliberately answers with an error rather than writing something the chain does not agree with. POST /api/nodes/sync takes a mint transaction hash or a node id and nothing else. It reads the owner out of the chain’s own NodeMinted log, then confirms that owner a second time against nodeInfo, and nothing the client says about ownership is used at any point.

StatusWhat it caught
400Neither a transaction hash nor a node id was sent, the transaction failed on chain, or it confirmed without minting a node
403The node is real, and the chain says it belongs to another wallet
404No node with that id has been minted, or the transaction has not confirmed and its mint log cannot be read yet
409The log named this wallet, but current state no longer does. Nothing is recorded rather than recording a node a reorg has stranded.

Missing this call entirely is survivable. The reconciler finds the same nodes from the chain later, so the route only decides how quickly the dashboard fills in, never whether the node exists.

GET /api/deploy-quote refuses in the same spirit. It quotes the payments address and the exact wei a transfer must carry, both of which come from the server’s configuration rather than from the contract. With no payments address configured it answers 503 rather than quoting a zero address, because a transfer sent against a quote of zeroes would be gone and would still not be a node.

Empty is not an error

  • A wallet with no nodes returns [] with status 200.
  • A protocol with no distributions returns [] with status 200.
  • /api/stats on a fresh deployment returns zeroes, not an error.
  • Nullable fields are null when unknown, never a placeholder value. In particular, "could not check" and "nothing found" are different answers and are represented differently.

Interfaces built on this API should say "no data yet" in those cases, which is what the charts throughout these docs do.