---
title: "Spin a mechanic and get a result"
method: POST
path: "/spin"
---

# Spin a mechanic and get a result

`POST /spin`

## Request

- Base URL: `https://localselections.com/api/v1`
- URL: `https://localselections.com/api/v1/spin`
- Auth: API key in header `X-API-Key`

Picks a weighted-random category (the same algorithm and cuisine/activity option pools the Wheel/Dice/RPS mechanics use) and narrows it to one real, concrete result via the matching local data source (Google Places, TMDB trending, or Ticketmaster) when enough location context is given. Records a decision_sessions row tied to the API key's owner, same as a browser spin. Requires the `spin` scope. Costs 3 units of the key's rate-limit budget per call.

## Request body

- object
  - `mechanic` 'wheel' | 'dice' | 'rps' | 'coinflip' | 'carddraw', required: Purely a record-keeping label — every value here picks from the same pool with the same algorithm. magic8ball isn't included: it's answer-only with no categoryMode-based result to narrow to, so it's only available via the group-session flow.
  - `categoryMode` 'cuisine' | 'activity'
  - `lat` number: Required (with lng) to narrow a cuisine pick or a place-based activity (hike/bowling/museum)/live-music pick.
  - `lng` number

## Response `200`

A picked category and, when narrowable, a real concrete result.

- object
  - `mechanic` string, required
  - `categoryMode` string, required
  - `winner` object, required
    - `id` string, required
    - `label` string, required
  - `narrowed` object, required: A concrete, real result for the winning category — shape depends on `source`.
    - `source` 'place' | 'movie' | 'event' | 'game-night' | 'none', required
    - `place` object
      - `id` string, required
      - `name` string, required
      - `distanceMiles` number, nullable
      - `address` string, nullable
      - `mapsUri` string, uri, required
      - `trending` object, nullable: Null only if the trending lookup itself failed — otherwise always present, with 0s when there's no real history yet.
        - `contributorCount` integer, required: Distinct users who were shown this place as their actual narrowed/won result, all-time.
        - `likedCount` integer, required: Subset of contributorCount who also left a real "liked" outcome vote.
        - `recentContributorCount` integer, required: Subset of contributorCount within the last 7 days.
      - `sponsored` boolean: True when this place has an active, admin-approved vendor boost (Stage 28) — always ranked among places that would organically appear in this search, never inserted separately.
      - `vendorDetails` object, nullable: Stage 185: the claimed vendor's own submitted hours/phone/website/menu highlights (Stage 184), when their listing is approved and at least one field has been submitted. Null otherwise. Takes display precedence over any Overture/Geoapify-sourced equivalent value for the same place.
        - `hours` string, nullable, required
        - `phone` string, nullable, required
        - `website` string, uri, nullable, required
        - `menuHighlights` string, nullable, required
      - `origin` 'overture' | 'geoapify' | 'google_places', required: Stage 191: which real source this place actually came from. Overture (local_selections.places, confidence-floored + deduped) and Geoapify (local_places) are tried first, free of any real API cost; Google Places is the fallback for whatever gap remains.
      - `detailUrl` string, uri, nullable: Stage 196: a real, permanent LocalSelections detail page for this exact place, reusing that page's own indexability rules (never a link to a page that would itself noindex, 301 elsewhere, or not exist). Null for every `google_places`-origin result (no permanent page exists for one) and for a local result that hasn't cleared the stricter indexing confidence floor or has an unresolved cross-source duplicate. Only present on /places (not /search, which predates this field).
    - `title` object
      - `id` string, required
      - `title` string, required
      - `mediaType` 'movie' | 'tv', required
      - `overview` string, nullable
      - `posterUrl` string, uri, nullable
      - `rating` number, nullable
      - `releaseDate` string, nullable
      - `tmdbUrl` string, uri, required: Link to the title at its source provider — TMDB, or IMDb when a fallback served it (see `provider`).
      - `provider` 'tmdb' | 'omdb' | 'tvmaze': Which provider produced this title. Absent means TMDB.
      - `detailUrl` string, uri, nullable: Stage 196: a real, permanent LocalSelections detail page for this exact title, reusing that page's own indexability rule (a TMDB-provider title with enough real overview text). Null for a non-TMDB-provider title (no local page exists) or one without enough overview text to index. Only present on /movies (not /spin or /bracket's narrowed/candidate titles, which predate this field).
    - `event` object
      - `id` string, required
      - `name` string, required
      - `venueName` string, nullable
      - `date` string, nullable
      - `time` string, nullable
      - `distanceMiles` number, nullable
      - `address` string, nullable
      - `imageUrl` string, uri, nullable
      - `ticketUrl` string, uri, required
      - `origin` 'ticketmaster' | 'user_submitted' | 'municipal' | 'web_schema' | 'rss_ical', required: Stage 182/183/186: which source this event came from — never blended indistinguishably. "municipal" is an official city open-data event calendar (currently New York, NY and Los Angeles, CA). "web_schema"/"rss_ical" are found directly on a business's own site (schema.org/JSON-LD markup, or a real structured RSS/iCal feed).
      - `originLabel` string: Stage 196: human-readable form of `origin` (e.g. "Ticketmaster", "the venue's own website"). Only present on /events (not /spin or /bracket's narrowed/candidate events, which predate this field).
      - `detailUrl` string, uri, nullable: Stage 196: a real, permanent LocalSelections detail page for this exact event, reusing that page's own content floor (enough real name/date/location, not ended). Always null for a `ticketmaster`-origin event — resolving it would require a second, separately-billed Ticketmaster API call per event, multiplying real spend against the same shared daily ceiling every other Ticketmaster call already draws from; use `ticketUrl` (the original listing) for those instead. Only present on /events (not /spin or /bracket, which predate this field).
    - `suggestion` object
      - `id` string, required
      - `name` string, required
      - `players` string, required
      - `blurb` string, required
    - `reason` string: Present only when source is "none" — why nothing was narrowed (e.g. missing lat/lng, no results found).
  - `decisionId` string, nullable

## Other responses

- `400`: The request body failed validation.
- `401`: Missing or invalid API key.
- `403`: The API key does not carry the scope this endpoint requires (see the "Requires the ... scope" note on each path, or call GET /introspect to see a key's real granted scopes).
- `429`: Rate limit exceeded for this API key. The budget is 30 units/60s for keys owned by an unsubscribed account, 120 units/60s for a subscribed account's keys (Stage 60) — shared across every endpoint, but each endpoint spends a different number of units per call rather than always 1 (Stage 104): search/groups_host/groups_join/places/movies cost 1, surprise_me/events cost 2, spin costs 3, bracket costs 4. Call GET /introspect to see a key's real remaining budget before it runs out.
- `502`: An upstream data source failed or returned nothing usable.

---

[API](https://skmtc.dev/localselections/apis/localselections-public-api.md) · [All operations](https://skmtc.dev/localselections/apis/localselections-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/localselections/apis/localselections-public-api/revisions/a24e9598add4?raw)
