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 prompt-guided only today — not code-enforced. Every other age-gated surface in this API (catalog, suggestions, shopping, ads) filters mature or excluded content in code, after the model runs, as a backstop. /plan and /extend do not yet have that backstop: the itinerary items are drawn from the model’s own knowledge (see Place metadata below), and only a prompt instruction asks it to avoid nightlife, bars, casinos, and adult-entertainment venues. A prompt instruction is not a guarantee.

Do not rely on /plan or /extend for Child, Tween, or Unknown-tier members until this is resolved. /destination/search and /destination/replace are unaffected — the model there only ever picks from an already-filtered candidate pool, which is a mechanically different, safe design. Contact your partner manager before using /plan/ /extend in any context where a passenger’s tier may not be Adult.

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. This is the mechanism behind the content-safety caveat at the top of this page: a model-invented place is composed into the response regardless of category, and a places-provider match (or lack of one) only affects whether address/phone/etc. get filled in — it does not filter anything.

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). There is no 429 for either endpoint today — a policy/ceiling denial surfaces as 500 internal.error.