# FastBrew API policy

FastBrew's canonical origin is https://fastbrew.app. The public HTTP API is
described by [OpenAPI 3.1](https://fastbrew.app/openapi.json) and discoverable
through the [API catalog](https://fastbrew.app/.well-known/api-catalog).
See the [developer guide](https://fastbrew.app/developers) for examples and the
[CLI](https://fastbrew.app/cli) for terminal access. General enquiries go to
[info@fastbrew.app](mailto:info@fastbrew.app).

## Version selection

Send `API-Version: 1` to select the current major version. Requests without the
header also select version 1. API responses include `API-Version: 1` and
`Vary: API-Version` (possibly alongside other fields). Unsupported versions
return HTTP 400 with `code: "unsupported_api_version"`; they are never silently
served as another version. The URLs remain `/api/apps`, `/api/categories`,
`/api/stats`, `/api/brewfile/parse`, `/api/brewfile/generate`, and `/api/health`.

Breaking changes require a new major version. Clients should explicitly select
their version and ignore new response fields. Catalog generations and package
versions are data, separate from the API version. `info.version` in OpenAPI
identifies the API description release, not the version-selection mechanism.

## Deprecation and retirement

Version 1 is active. No version is deprecated and no retirement is scheduled.
Active responses therefore omit `Deprecation` and `Sunset`.

Before retiring a version, FastBrew will publish migration instructions and the
retirement date here, mark affected OpenAPI operations as `deprecated: true`,
and send these headers on affected responses:

- `Deprecation`: an RFC 9745 Structured Field Date (`@` followed by Unix seconds)
  indicating when deprecation takes effect.
- `Sunset`: an RFC 8594 HTTP-date in GMT indicating when the version stops
  responding. It must not precede the deprecation date.
- `Link: <https://fastbrew.app/api-policy.md>; rel="deprecation"; type="text/markdown"`
  linking to the migration instructions.

There is no guaranteed minimum notice period before retirement. Clients should
monitor these headers and this policy while using the API.

## Errors and retries

All application responses with HTTP status 400–599 under `/api` or `/api/` use
`application/json`, regardless of `Accept`. HEAD responses have the same status
and headers without a body. Errors include:

```json
{
  "error": "Not found",
  "code": "not_found",
  "hint": "Check the endpoint and resource identifier in /openapi.json."
}
```

Use `code` for decisions, `error` for the explanation, and `hint` for recovery.
The stable codes are `invalid_request`, `unsupported_api_version`, `unauthorized`,
`forbidden`, `not_found`, `method_not_allowed`, `catalog_expired`,
`payload_too_large`, `unsupported_media_type`, `rate_limited`, `internal_error`,
`service_unavailable`, and `request_failed` for other HTTP errors.

For 405, use the `Allow` header. For 429, wait for `Retry-After` before retrying.
For 410, restart catalog pagination at page zero without a generation.
For server failures, retry with backoff and retain `X-Request-ID` for diagnostics.
An unhealthy `/api/health` response also retains its health status and checks.
Errors are not cached. Infrastructure failures outside the application, such as
a Cloudflare access block, may have a different response format.

## Rate limits

Catalog search requests (`GET /api/apps` with a nonempty `search`) share a limit
of 30 requests per 60 seconds with browser catalog searches. Brewfile parse and
generate requests each have a separate limit of 10 requests per 60 seconds.
These limits are per client IP address, within each Cloudflare location; clients
sharing a public IP share the allowance. Enforcement is approximate and cached
responses can avoid origin work. Unfiltered catalog reads do not consume the
search allowance. Infrastructure protections can impose additional limits.

When the relevant limiter is configured, catalog and Brewfile responses advertise
its policy using the Structured Fields syntax from draft-ietf-httpapi-ratelimit-headers-11.
Catalog reads advertise the search policy even without a search term, so clients
can discover the allowance in advance; only searches consume that allowance:

```http
RateLimit-Policy: "catalog-search";q=30;w=60
```

An exhausted quota returns HTTP 429 with JSON code `rate_limited` and:

```http
RateLimit: "catalog-search";r=0;t=60
Retry-After: 60
```

The other policy identifiers are `brewfile-parse` and `brewfile-generate`.
The limiter returns only an allow/reject decision, so successful responses omit
`RateLimit` rather than inventing a remaining count. The 60-second retry interval
is conservative; it is not an exact reset timestamp or a guarantee of capacity.
The RateLimit fields follow an Internet-Draft, not a final RFC.

## Standards

- [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0.html)
- [API catalogs: RFC 9727](https://www.rfc-editor.org/rfc/rfc9727.html)
- [Deprecation: RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html)
- [Sunset: RFC 8594](https://www.rfc-editor.org/rfc/rfc8594.html)
- [RateLimit draft 11](https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-11.html)
