Destination Search & Replace

POST /v1/destination/search and POST /v1/destination/replace — POI discovery for a destination, member-scored.

Two calls curate points of interest (dining and experiences) for a member’s destination:

  • POST /v1/destination/search — a fresh, two-category candidate list.
  • POST /v1/destination/replace — swap one place out for a new pick, without disturbing the rest of what you’re showing.

Both are session-scopedrequire the X-Session-Id header.

Body (max 64 KiB):

FieldRequiredRules
seatIdyesNon-blank.
memberSlotnoOmit for solo.
destinationCityyesNon-blank. Flat field — see the note below.
destinationCountryyesNon-blank. Flat field.

Search takes destinationCity/destinationCountry as flat top-level fields. Replace, and the plan/extend endpoints, take a nested destination: {city, country, …} object instead. The two shapes are genuinely different — don’t reuse one request builder for both.

$curl -X POST "https://api.staging.westiq.ai/v1/destination/search" \
> -H "Authorization: Bearer $ACCESS_TOKEN" \
> -H "X-Session-Id: seat-12A-f3d9c1" \
> -H "Content-Type: application/json" \
> -d '{ "seatId": "12A", "destinationCity": "Rome", "destinationCountry": "Italy" }'
1{
2 "schemaVersion": "1",
3 "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
4 "dining": [
5 {
6 "id": "place_trattoria",
7 "name": "Trattoria da Enzo",
8 "category": "dining",
9 "neighborhood": "Trastevere",
10 "matchScore": 82,
11 "whyForYou": "Matches your love of local, unfussy spots"
12 }
13 ],
14 "experiences": [
15 {
16 "id": "place_colosseum",
17 "name": "Colosseum",
18 "category": "cultural",
19 "matchScore": 74
20 }
21 ]
22}

Each place carries an id/name plus provider-merged-or-null fields (category, neighborhood, priceTier, address, phone, rating, reviewCount, imageUrl, sourceUrl — omitted individually when the places provider has no data for that field), a code-clamped matchScore (0–100 integer), and optional model free text in whyForYou.

On a search result, category is omitted whenever the places provider didn’t supply one — don’t treat it as guaranteed. On a replace result, it always falls back to a generic label when the provider gives nothing, so it’s always present there.

Replace

Body (max 64 KiB):

FieldRequiredRules
seatIdyesNon-blank.
memberSlotnoOmit for solo.
destinationyesNested { city, country, airport?, lat?, lon? } — city and country non-blank.
dismissedPlaceIdyesThe place being swapped out. Non-blank.
timeBlockSlotnoOne of morning / lunch / afternoon / evening, if you want the replacement scoped to a slot. Any other value → 422.
pinnedPlaceIdsnoPlaces to never suggest as the replacement. Default [], max 100.
$curl -X POST "https://api.staging.westiq.ai/v1/destination/replace" \
> -H "Authorization: Bearer $ACCESS_TOKEN" \
> -H "X-Session-Id: seat-12A-f3d9c1" \
> -H "Content-Type: application/json" \
> -d '{
> "seatId": "12A",
> "destination": { "city": "Rome", "country": "Italy" },
> "dismissedPlaceId": "place_trattoria"
> }'
1{
2 "schemaVersion": "1",
3 "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
4 "replacement": {
5 "name": "Roscioli",
6 "category": "dining",
7 "matchScore": 79
8 },
9 "reasoning": "A similarly local pick, different neighborhood"
10}

replacement (an itinerary-item shape carrying the same provider-merged-or-null fields as a search result, plus hours when known) and reasoning are omitted together whenever no replacement could be produced — never a fabricated place.

Business logic

  • Search fetches dining and experience candidates independently; if both categories come back empty, the platform makes no model call and returns the honest-empty shape (dining: [], experiences: []) at zero cost.
  • Replace’s candidate pool is the union of both categories, minus pinnedPlaceIds and the dismissedPlaceId itself, fetched fresh on every call (not cached). The model picks exactly one replacement from that offered pool; if it names something outside the pool, the response is the same honest-null shape — not a fabricated place, and not an error.

Places-provider degradation is the current default in the sandbox, not an edge case. If your environment’s places provider is disabled, /search returns dining: [], experiences: [] and /replace returns replacement omitted on every call — by design. Don’t read an empty result as a bug during initial integration; confirm with your partner manager whether live place data is enabled for your environment.

Errors

Standard envelope. Both: 400 session.unresolved, 401 / 403, 500. Search additionally: 422 request.validation (blank seatId, destinationCity, or destinationCountry). Replace additionally: 422 request.validation (blank seatId, blank destination.city/destination.country, blank dismissedPlaceId, an invalid timeBlockSlot, or more than 100 pinnedPlaceIds). There is no 429 for either endpoint today — a policy/ceiling denial surfaces as 500 internal.error.