

# API errors [#api-errors]

The HTTP API wraps expected calculator service failures in
`CalculatorApiErrorEnvelope`. The `error` field is the canonical
`CalculatorServiceError` union from the calculator package.

## Error model [#error-model]

```json
{
  "error": {
    "_tag": "CalculatorInputDecodeError",
    "calculatorId": "au.pay.take-home",
    "message": "Invalid facts for au.pay.take-home",
    "issues": [
      {
        "_tag": "CalculatorInputIssue",
        "message": "Expected GrossPay",
        "path": ["grossPay"]
      }
    ]
  }
}
```

Expected failures stay in the service error channel. The current public union
includes calculator input decode failures, unsupported calculator errors,
unsupported calculator context errors and calculation errors. Those request
failures use HTTP 400. A busy calculation server returns HTTP 503 with
`CalculatorCapacityExceeded`. An operation that exceeds five seconds returns
HTTP 504 with `CalculatorOperationTimedOut`. Both include a fixed `code`,
`message` and `retry: "try-again-manually"`. Display the message and let the
caller choose when to retry; do not automatically repeat calculations.

The server shares eight active calculation places across HTTP and RPC. Each
calculation in a batch takes a place. This budget does not claim that a
JavaScript timer can forcibly stop synchronous CPU work. Current per-client
rate admission in the native API adds 60 calculations per minute per checked
connection address across HTTP, RPC and Website forms. Every batch calculation
counts separately; metadata reads do not count. A rejected calculation returns
HTTP 429 with `CalculatorRateLimited` and `Retry-After: 60`. Missing/invalid
connection identity or a failed limiter returns HTTP 503 with
`CalculatorAdmissionUnavailable`. Both contain fixed safe guidance and manual
retry fields. These native limits are approximate and local to a Cloudflare
location, rather than an exact worldwide quota. The standalone Bun server
keeps its existing work policy.

## Handle an error response [#handle-an-error-response]

```ts
import { CalculatorApiErrorEnvelope } from "@taxkit/api-http";
import { Effect, Schema } from "effect";
import type { HttpClientResponse } from "effect/http/HttpClientResponse";

// A response from an HTTP request is untrusted until its owning Schema checks it.
export const readCalculatorError = (response: HttpClientResponse) =>
  response.json.pipe(
    Effect.flatMap(Schema.decodeUnknownEffect(CalculatorApiErrorEnvelope))
  );
```

Call `readCalculatorError` with the failed calculation response from an Effect
HTTP client. The returned `body.error` has the owning calculator error type.
If the response does not match that contract, decoding returns a Schema error.

For typed HTTP-client calls, endpoint error bodies are already checked; use the
error channel shown in [Build a browser app](/guides/build-a-browser-app-with-the-http-api).
Do not maintain another error-envelope type or force an unchecked JSON value
into one with a cast.

## Add help [#add-help]

Append `?help=errors` when you want descriptor-backed help for invalid input:

```txt
POST /api/v1/calculators/au.pay.take-home/calculate?help=errors
```

The help data comes from canonical fact descriptors. Do not maintain a
separate application copy of calculator fields.

## Public documentation errors [#public-documentation-errors]

The documentation routes return their own error JSON. Read `_tag` and
`message` directly; these replies do not use `CalculatorApiErrorEnvelope`.

| Status | `_tag`                  | Message                                            | What to do                             |
| ------ | ----------------------- | -------------------------------------------------- | -------------------------------------- |
| `404`  | `DocsPageUnavailable`   | `The documentation page was not found.`            | Choose a page address from navigation. |
| `503`  | `DocsSearchUnavailable` | `Documentation search is temporarily unavailable.` | Try your search again later.           |

Both the page JSON and Markdown routes return the same missing-page error.
A missing or invalid `path` or `term` returns an empty HTTP 400 reply. Check
that you supplied the required query and used the limits in
[Endpoints](/api/endpoints).

## Related pages [#related-pages]

* [Handle validation errors](/guides/handle-validation-errors)
* [Show calculator help to users](/guides/show-calculator-help-to-users)
* [Schemas](/sdk/schemas)
