---
title: "Explore Taste Reveal"
method: POST
path: "/explore/taste/reveal"
tags: ["explore"]
---

# Explore Taste Reveal

`POST /explore/taste/reveal`

Open the reward the "import 3 reels" contract promised, and serve the
board it produces in the same round trip. A reveal IS the Explore board, so
it takes no surface.

409 below the target: the reveal is a promise kept, so a client that lost
track of the count must not be able to open it early. Idempotent — the
`IS NULL` guard keeps the first stamp, which is what `cta.state` is read
from.

## Query parameters

- `k` integer
- `n_rails` integer
- `session_id` string, nullable

## Headers

- `x-client-build` string, nullable

## Response `200`

Successful Response

- TasteRailsResponse — A user's taste rails: reel collections, then their own clusters, then Trending themes. Empty `rails` is the normal answer for a user with too few saves to cluster — never an error. `cta` is absent only when its read failed; the client then renders rails alone rather than the wrong cover.
  - `request_id` string, required
  - `rails` TasteRailResponse[]
    - `request_id` string, required
    - `cluster_index` integer, required
    - `title` string, nullable
    - `cluster_size` integer, required
    - `items` ExploreItem[], required
      - `place_id` string, required
      - `name` string, required
      - `category` string, nullable
      - `city` string, nullable
      - `country_code` string, nullable
      - `lat` number, nullable
      - `lng` number, nullable
      - `image_url` string, nullable
      - `image_thumbhash` string, nullable
      - `rating` number, nullable
      - `num_reviews` integer, nullable
      - `description` string, nullable
      - `michelin_award` string, nullable
      - `michelin` MichelinDistinction — A place's Michelin Guide distinction, structured. Canonical rationale — the TS mirror and the renderers point here rather than restating it. Rides alongside `michelin_award` rather than replacing it: that field is a pre-rendered label ("★★★"), and a label cannot drive iconography. An award string we don't recognise yields NO distinction (we never paint a mark we can't justify) while still surfacing verbatim in `michelin_award`, so a new tier degrades to text rather than disappearing.
        - `tier` 'stars' | 'bib_gourmand' | 'selected', required
        - `stars` integer, nullable
        - `green_star` boolean
        - `url` string, nullable
        - `cuisine` string, nullable
      - `source` string, required
      - `score` number, required
      - `position` integer, required
      - `reason` string, nullable
      - `hydration_pending` boolean
      - `sources` string[]
      - `kind` 'place' | 'stay'
      - `stay` StayFacets — What a Michelin Guide hotel carries that an ordinary place doesn't. Same rationale as `MichelinDistinction`, one category later: `key_label` is the Guide's own pre-rendered string ("Two MICHELIN Keys"), and a label cannot drive iconography, so `tier`/`keys` ride beside it. A tier we don't recognise yields no structured distinction while still surfacing verbatim in `key_label` — a new tier degrades to text rather than disappearing. A sub-object rather than flat columns on `ExploreItem`: otherwise every future category (a tour, a tasting menu) adds its own nullable fields to the type EVERY card in the app carries.
        - `key_label` string, nullable
        - `tier` 'keys' | 'selected', nullable
        - `keys` integer, nullable
        - `guide_url` string, nullable
        - `editorial` string, nullable
        - `check_in` string, date, nullable
        - `check_out` string, date, nullable
      - `distance_km` number, nullable
      - `distance_from` 'you' | 'hotel', nullable
    - `exemplars` string[]
    - `kind` 'reels' | 'own' | 'trending'
    - `donors` integer, nullable
  - `cta` TasteCta — Slot 1 of the Explore tab — the "import 3 reels" contract and its reward (docs/TASTE_FOR_YOU_TRENDING.md §5.5). `target` is served rather than hardcoded by the client because it is an activation knob. `themes` is present only while the CTA is still asking (`invite`, `progress`, `ready`); once revealed the reel rails are the cover.
    - `state` 'invite' | 'progress' | 'ready' | 'revealed' | 'graduated', required
    - `reels` integer, required
    - `target` integer, required
    - `last_place` string, nullable
    - `themes` TasteRailResponse[]
      - `request_id` string, required
      - `cluster_index` integer, required
      - `title` string, nullable
      - `cluster_size` integer, required
      - `items` ExploreItem[], required
        - `place_id` string, required
        - `name` string, required
        - `category` string, nullable
        - `city` string, nullable
        - `country_code` string, nullable
        - `lat` number, nullable
        - `lng` number, nullable
        - `image_url` string, nullable
        - `image_thumbhash` string, nullable
        - `rating` number, nullable
        - `num_reviews` integer, nullable
        - `description` string, nullable
        - `michelin_award` string, nullable
        - `michelin` MichelinDistinction — A place's Michelin Guide distinction, structured. Canonical rationale — the TS mirror and the renderers point here rather than restating it. Rides alongside `michelin_award` rather than replacing it: that field is a pre-rendered label ("★★★"), and a label cannot drive iconography. An award string we don't recognise yields NO distinction (we never paint a mark we can't justify) while still surfacing verbatim in `michelin_award`, so a new tier degrades to text rather than disappearing.
          - `tier` 'stars' | 'bib_gourmand' | 'selected', required
          - `stars` integer, nullable
          - `green_star` boolean
          - `url` string, nullable
          - `cuisine` string, nullable
        - `source` string, required
        - `score` number, required
        - `position` integer, required
        - `reason` string, nullable
        - `hydration_pending` boolean
        - `sources` string[]
        - `kind` 'place' | 'stay'
        - `stay` StayFacets — What a Michelin Guide hotel carries that an ordinary place doesn't. Same rationale as `MichelinDistinction`, one category later: `key_label` is the Guide's own pre-rendered string ("Two MICHELIN Keys"), and a label cannot drive iconography, so `tier`/`keys` ride beside it. A tier we don't recognise yields no structured distinction while still surfacing verbatim in `key_label` — a new tier degrades to text rather than disappearing. A sub-object rather than flat columns on `ExploreItem`: otherwise every future category (a tour, a tasting menu) adds its own nullable fields to the type EVERY card in the app carries.
          - `key_label` string, nullable
          - `tier` 'keys' | 'selected', nullable
          - `keys` integer, nullable
          - `guide_url` string, nullable
          - `editorial` string, nullable
          - `check_in` string, date, nullable
          - `check_out` string, date, nullable
        - `distance_km` number, nullable
        - `distance_from` 'you' | 'hotel', nullable
      - `exemplars` string[]
      - `kind` 'reels' | 'own' | 'trending'
      - `donors` integer, nullable

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/stardrift/apis/fastapi.md) · [All operations](https://skmtc.dev/stardrift/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/stardrift/fastapi/revisions/00746572572a/schema)
