Skip to navigation

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-scoped — require 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" }'
{
"schemaVersion": "1",
"correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
"dining": [
{
"id": "place_trattoria",
"name": "Trattoria da Enzo",
"category": "dining",
"neighborhood": "Trastevere",
"matchScore": 82,
"whyForYou": "Matches your love of local, unfussy spots"
}
],
"experiences": [
{
"id": "place_colosseum",
"name": "Colosseum",
"category": "cultural",
"matchScore": 74
}
]
}

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"
}'
{
"schemaVersion": "1",
"correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
"replacement": {
"name": "Roscioli",
"category": "dining",
"matchScore": 79
},
"reasoning": "A similarly local pick, different neighborhood"
}

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.

Place screening and categories

For Child, Tween and Unknown members, candidate screening checks every supplied usable category for age-inappropriate venues, and checks names for explicit adult/nightlife terms. If no usable category exists, the name also receives the broader venue screening. Ordinary family venues such as golf clubs and kids clubs are not excluded merely for the word “club”; nightlife venues remain excluded. The returned category uses the first usable category, so an initial placeholder does not hide a later real category. This rule does not enable the places provider: the disabled-provider empty results described above still apply.

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). Both endpoints enforce the per-client request-rate limit: over it, 429 rate.limited with a Retry-After header. Other policy/ceiling denials still surface as 500 internal.error.