---
title: "Local search by category"
method: POST
path: "/search"
---

# Local search by category

`POST /search`

## Request

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

Real nearby results for one cuisine or place-based activity category (hike/bowling/museum) — the same Google Places integration the browser wheel's category search uses. Stage 153: this app no longer requests Google's rating/userRatingCount/priceLevel/currentOpeningHours fields for any of the six v1 endpoints (they bill at a materially more expensive Places SKU) — `NearbyPlace.trending` carries this app's own real, free first-party selection/liked-history signal instead (see the NearbyPlace schema), and `sponsored` is unaffected. Movie/live-music/game-night categories have no nearby-places search of their own; use /spin or /surprise-me for those. Requires the `search` scope. Costs 1 unit of the key's rate-limit budget per call — the cheapest of the six v1 endpoints.

## Request body

- object
  - `categoryMode` 'cuisine' | 'activity', required
  - `optionId` string, required: e.g. "sushi" for cuisine, or "hike"/"bowling"/"museum" for activity.
  - `lat` number, required
  - `lng` number, required

## Response `200`

Nearby places for the requested category.

- object
  - `places` object[], required
    - `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).

## 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)
