Destination Plan & Extend

POST /v1/destination/plan and POST /v1/destination/extend — a per-member daily itinerary, code-dated.

Two calls build a multi-day itinerary for a member:

  • POST /v1/destination/plan — a fresh itinerary, starting at day 0.
  • POST /v1/destination/extend — append more days to an itinerary your client is already holding.

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

Content-category safety on these two endpoints is code-enforced. Like every other age-gated surface in this API (catalog, suggestions, shopping, ads), /plan and /extend filter the composed output in code, after the model runs: for a restricted member — Child, Tween, or Unknown tier, with Unknown treated as Child, per the age-tier model — any nightlife, bar, casino, gambling, or adult-category item is dropped from the response, regardless of what the model composed. You may serve /plan and /extend to members of every tier.

Two consequences to design for: a restricted member’s time blocks and arrival recommendations can honestly contain fewer items than an adult’s for the same trip — sparser by design, never padded with substitutes — and adult members’ itineraries are unchanged by this filter.

Plan

Body (max 64 KiB):

FieldRequiredRules
seatIdyesNon-blank.
memberSlotnoOmit for solo.
destinationyesNested { city, country, airport?, lat?, lon? } — city and country non-blank.
tripDurationNightsyesInteger, 1–7 inclusive. There is no date field — see Dates below.
originnoSame locator shape as destination; used only for arrival-narrative context.
$curl -X POST "https://api.staging.westiq.ai/v1/destination/plan" \
> -H "Authorization: Bearer $ACCESS_TOKEN" \
> -H "X-Session-Id: seat-12A-f3d9c1" \
> -H "Content-Type: application/json" \
> -d '{
> "seatId": "12A",
> "destination": { "city": "Lisbon", "country": "Portugal" },
> "tripDurationNights": 3
> }'
1{
2 "schemaVersion": "1",
3 "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
4 "destination": { "city": "Lisbon", "country": "Portugal" },
5 "days": [
6 {
7 "dayIndex": 0,
8 "date": "2026-08-01",
9 "dayLabel": "Day 1",
10 "description": "Arrival day",
11 "timeBlocks": [
12 { "slot": "morning", "items": [{ "id": "place_cafe_a_brasileira", "name": "Café A Brasileira", "category": "cafe" }] },
13 { "slot": "lunch", "items": [{ "name": "Time Out Market", "category": "dining" }] },
14 { "slot": "afternoon", "items": [{ "name": "Belém Tower", "category": "cultural" }] },
15 { "slot": "evening", "items": [{ "name": "Fado dinner in Alfama", "category": "dining" }] }
16 ]
17 }
18 ],
19 "arrival": {
20 "transitInstructions": "Take the Aerobus into the city center",
21 "jetLagTip": "Stay hydrated on arrival",
22 "eveningRecs": [{ "id": "place_cafe_a_brasileira", "name": "Café A Brasileira", "category": "cafe" }]
23 },
24 "weatherAvailable": true
25}
  • heroImage is declared on the response but always omitted today — call /destination/hero-image separately for a banner image.
  • days[].description and each item’s descriptive text are model free text, output-guarded.

Extend

Body (max 64 KiB):

FieldRequiredRules
seatIdyesNon-blank.
memberSlotnoOmit for solo.
destinationyesSame nested locator shape.
existingDaysyesInteger ≥ 0 — how many days your client already holds.
newTotalDaysyesInteger ≥ 0 — the total day count you want after this call. newTotalDays - existingDays must not exceed 7.
pinnedPlaceIdsnoPlaces to keep out of the new days. Default [], max 100.
weathernoA client-supplied forecast for the new days (same shape as the response’s days[].weather). If you supply one, the server trusts it verbatim instead of fetching its own.
$curl -X POST "https://api.staging.westiq.ai/v1/destination/extend" \
> -H "Authorization: Bearer $ACCESS_TOKEN" \
> -H "X-Session-Id: seat-12A-f3d9c1" \
> -H "Content-Type: application/json" \
> -d '{
> "seatId": "12A",
> "destination": { "city": "Lisbon", "country": "Portugal" },
> "existingDays": 3,
> "newTotalDays": 5
> }'
1{
2 "schemaVersion": "1",
3 "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d",
4 "newDays": [
5 { "dayIndex": 3, "date": "2026-08-04", "dayLabel": "Day 4", "timeBlocks": [] }
6 ]
7}

newDays carries only the newly generated days — never the full itinerary; your client already holds the earlier days, and the server holds no itinerary state of its own between calls.

A newTotalDays that isn’t greater than existingDays is a valid, cost-free request: it short-circuits to {"newDays": []} with zero model calls. If the model returns more days than you asked for, the response is clipped to the exact count; if fewer, it’s left honestly short — never padded.

Dates are entirely code-computed

Neither request has a date field. /plan sends tripDurationNights; /extend sends existingDays/newTotalDays. The server resolves “today” from its own clock at request time, and each day’s date is pure arithmetic from that anchor — the model is never asked for a date at all.

/extend re-anchors “today” on every call — it does not remember the original plan’s start date. If you call /extend on a different calendar day than the original /plan call, the new days’ dates are computed from that call’s “now,” which may not line up with day 0 of the itinerary you’re extending. This is fine for extending within the same session, but don’t assume date continuity across a stale, multi-day-old plan.

Place metadata: provider-merged or null

Every itinerary item’s address, phone, hours, rating, reviewCount, imageUrl, and sourceUrl start unset. They are filled in only when the model-asserted place name matches an entry from the places provider’s results for that destination, fetched in parallel with the model call — never guessed, never fabricated. The model itself generates the place names and categories from its own knowledge; the places-provider pool is used only for this metadata overlay, never shown to the model as a candidate list to choose from. A places-provider match (or lack of one) only affects whether address/phone/etc. get filled in — the overlay itself filters nothing. Content-category filtering for restricted members is a separate, code-enforced step on the composed output (the note at the top of this page).

Weather

weatherAvailable (also the field name on the dedicated /destination/weather endpoint) reports whether the platform could resolve a forecast for this trip: coordinates are resolved from lat/lonairportcity/country, in that order, against a small reference table. If resolution fails, or the weather provider call itself fails, weatherAvailable is false and every day’s weather is null — never a fabricated forecast. This is a separate code path from the dedicated weather endpoint, though the semantics match.

If your environment’s places provider is disabled, expect every itinerary item’s provider-merged fields to be null on every call — legitimate honest degradation, not a bug. The weather provider is keyless and always on, so weatherAvailable should behave normally regardless.

Errors

Standard envelope. Both: 400 session.unresolved, 401 / 403, 500. Plan additionally: 422 request.validation (blank seatId, blank destination.city/.country, or tripDurationNights outside 1–7). Extend additionally: 422 request.validation (blank seatId, blank destination.city/.country, a negative existingDays/newTotalDays, a delta over 7, 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.