# REST conventions

Base: `https://sunglassesguides.com/api/v1`. OpenAPI: https://sunglassesguides.com/openapi.json. JSON in and out, UTF-8. Request bodies up to 16 KB.

## Envelope

| Field | Meaning |
|---|---|
| request_id | Id of this request (also `X-Request-Id`). Not a transaction receipt. |
| success | The request was processed correctly. Not "something matched". |
| schema_version | Envelope/field schema version (1.0). |
| data_version | Published catalog + offer batch version the answer was computed from, e.g. `c1.o0`. |
| data_mode | `production` (real records only) or `demo` (synthetic fixtures). |
| generated_at | When this response was generated (UTC). Not a product fact time. |
| request | Normalized operation and parameters (ZIP truncated, no secrets). |
| result | Operation result; business status in `result.status`. |
| sources | Deduplicated sources cited by fields in `result`. |
| freshness | Spec and offer freshness for the records in this response. |
| cache | This site's cache only (see [freshness](https://sunglassesguides.com/docs/freshness.md)). |
| next_actions | Optional read-only follow-up calls, never instructions or authorization. `requires_user_confirmation:true` marks any relaxation of a hard condition. |
| error | `null` on success. On failure: `code`, `message`, `retryable`, optional `retry_after_seconds`, `details`. |

## HTTP status

200 for processed requests including no-match, NEEDS_VARIANT and NO_OFFER_COVERAGE. 400 malformed JSON or cursor, 401 unknown/malformed key, 403 revoked key or wrong environment, 404 unknown explicit id, 405 wrong method, 409 DATA_VERSION_CHANGED, 413 body too large, 422 invalid parameters or UNSUPPORTED_MARKET, 429 quota, 500 internal, 503 database unavailable. See [errors](https://sunglassesguides.com/docs/errors.md).

## Consistency across calls

Pass the `data_version` you received as `expected_data_version` to follow-up calls (find with reference_variant_id, compare, variant, offers, next page). If the catalog changed you get 409 `DATA_VERSION_CHANGED` with `result.current_data_version`: re-run the ORIGINAL query without `expected_data_version`, keep every hard condition, then continue with the new version. Cursors are bound to the query and version. Offer freshness is still checked against the current time even when the version is unchanged.

## Pagination and size

find returns 5 matches by default (max 20) sorted by `size_distance_mm` then `variant_id`; use `result.page.next_cursor`. At most 5 undecided candidates are listed. compare accepts 2-4 ids.

## Headers

`X-Request-Id`, `X-Data-Version`, `RateLimit-Policy`, `RateLimit`, `X-RateLimit-*`, `Retry-After` (429/503), `ETag` on GET (conditional requests with `If-None-Match` return 304; quota still applies). Optional request header `X-Correlation-Id` (opaque, <= 64 chars, no personal data) groups calls of one task in usage statistics.
