# A trip: making one, and changing it afterwards

> Create a trip, describe who was on it, and change its own fields afterwards.

## Creating a trip

A trip's id is client-chosen — you `PUT` it into existence rather than `POST` to a collection. It is not permanent: `POST .../rename` below changes it, when it was chosen wrong.

```http
PUT /api/v2/{user}/trips/{trip}
Content-Type: application/json
Authorization: Bearer fs_agent_…

| Field | | Type |
| --- | --- | --- |
| `id` | **required** | string — must match the URL's `{trip}` — lowercase words joined by hyphens |
| `title` | **required** | string — what the trip is called |
| `dates` | **required** | an object — see /api/v2/openapi.json for its shape — `{from, to}`, both `YYYY-MM-DD` — required, never guessed |
| `visibility` | **required** | one of `private`, `public`, `guest` — `private`, `public` or `guest` — an explicit choice, never a guessed default |
| `people` | **required** | a list of {an object — see /api/v2/openapi.json for its shape} — who was on it — `[{name, email, nickname?}]`, 1 to ten. The byline only — it grants no write access and mails nobody; a buddy is added and granted from Studio › Readers, in the owner's own browser |
| `teaser` | optional | boolean — required on a closed trip only: may its existence show as a locked card nobody may open? |
| `rates` | optional | an object — see /api/v2/openapi.json for its shape — `{currencies: ["JPY"], manual?: {"JPY": 148.2}}` — manual rates are units per 1 EUR — or decline it |
| `costs` | optional | an object — see /api/v2/openapi.json for its shape — a budget and preparation spend, or decline it (see /skill/costs.md) |
| `plan` | optional | an object — see /api/v2/openapi.json for its shape — an upcoming trip's intended route, or decline it |
| `days` | optional | a list of {an object — see /api/v2/openapi.json for its shape} — a trip's days, or decline it — a fresh trip sends an empty list and writes each day through its own door instead |
| `translations` | optional | an object — see /api/v2/openapi.json for its shape — the trip's title/tagline/intro in the journal's other languages, or decline it |
| `accent` | optional | one of `sky`, `yellow`, `green`, `coral`, `navy` — the trip's colour, or decline it |
| `cover` | optional | string |
| `figures` | optional | an object — see /api/v2/openapi.json for its shape or an object — see /api/v2/openapi.json for its shape or an object — see /api/v2/openapi.json for its shape — how the party is drawn: `{"mode":"off"}`, `{"mode":"journal"}` or `{"mode":"custom","figures":[ids]}`, or decline it |
| `tagline` | optional | string — one line under the title, or decline it |
| `intro` | optional | string — the trip page's opening prose, or decline it |
| `listed` | optional | boolean — public trips only: is it advertised (sitemap, feed, switcher)? |
| `reminder` | optional | an object — see /api/v2/openapi.json for its shape |
| `declined` | optional | an object — see /api/v2/openapi.json for its shape — which asked-or-declined sections were consciously left out, and why (10+ characters) |
| `test` | optional | boolean — `true` only for content nobody lived |
```

## Every section is asked, or declined — never silently missing

A trip is not a form with defaults: `rates`, `costs`, `plan`, `days`, `translations`, `accent`, `figures`, `tagline` and `intro` are each either sent, or named in `declined` with a real reason (ten characters or more — `"n/a"` does not satisfy it). Sending neither is refused with `422 incomplete`, naming exactly which sections are missing and what to send for each. **Never invent a value to get past this** — decline it, or go and ask.

**`teaser` and `listed` are conditional on `visibility`.** A closed trip (`private`/`guest`) must answer `teaser` and must not send `listed`; a `public` trip must answer `listed` (declinable) and must not send `teaser`. Sending the wrong one for the trip's own visibility is refused.

**`cover` is not a field at create.** No photograph can exist yet, so the question has no honest answer here — it becomes writable once the trip holds media, via `PATCH`.

## Planning a trip

`plan` is a route of stops for a trip that has not happened yet — write it, or decline it, like every other section above:

```json
{"plan": {"mode": "nights", "readers": "map", "route": [{"location": "Bangkok", "lat": 13.7563, "lng": 100.5018, "nights": 4}, {"location": "Chiang Mai", "lat": 18.7883, "lng": 98.9853}]}}
```

**Each stop gets a stable `id` for free.** Leave it out and the server assigns a slug of `location`, unique within the route — write your own only when you need to name it before you know what the server would have chosen. `costs.items[].stop` (see /skill/costs.md) and `plan.private.stops` (below) both key off this same id.

**`mode` decides who owns `arrive`/`leave`.** `nights` (the default) is the server's: every time the route is written, it derives each stop's `arrive`/`leave` from the trip's own start date, the route's order and each stop's `nights` — sending `arrive`/`leave` yourself in this mode is pointless, since the next write recomputes them anyway. A stop with no `nights` of its own keeps whatever `arrive` the chain already reached, loses its own `leave`, and every later stop loses both — there is no way to know when an unstated stay ends. `dates` mode is the opposite: `arrive`/`leave` are taken exactly as sent, gaps and overlaps included, and nothing is derived. Switch back to `nights` without resending `nights` and it is recounted from whatever dates `dates` mode left in place.

**`see`** is an optional list of `{name, lat, lng, source?}` — places worth visiting at a stop, never a bed and never a bill. `source` (on `see` and on a stop itself) says how the coordinates were arrived at: `typed`, `maps-link`, `pin` or `coordinates`.

**`plan.private` is the owner's alone.** `{links?: [{label,url}], stops?: {[id]: {stay?: {name,lat,lng}, links?: [{label,url}]}}}` — a hotel booked, a confirmation link, anything that must never reach the public map. `GET` drops the whole key for anyone who is not the owner; a `stops` key that names an id not in `route` is refused, naming it. Put a hotel here, never as an ordinary stop — a stop is drawn on the map for everyone who may read the trip.

**`readers`** says whether the trip's readers get the map alone (`map`, the default) or the fuller stop-by-stop detail (`details`) — never `plan.private`, which `readers` has no power to widen.

## Reading, correcting, deleting

```http
GET    /api/v2/{user}/trips              — every trip, paged
GET    /api/v2/{user}/trips/{trip}       — one trip, with its days inline
PATCH  /api/v2/{user}/trips/{trip}       — merge-patch; nothing is asked, but supplying a previously declined section clears the decline
DELETE /api/v2/{user}/trips/{trip}       — owner only. Answers 202 and deletes nothing: a mail goes to the owner, and only its button deletes
```

**A PATCH does not ask anything new, but the merged document is re-validated in full** — the same asked-or-declined check a create runs, against the trip as it stands after your patch is laid over it. A section already answered or declined stays satisfied without you resending it; `cover` is the one exception, since it only becomes a real question once the trip holds media — a first photograph landing on the trip does not retroactively refuse an old PATCH, but a later one that would leave the trip without an answer for it does.

`PATCH` refuses `days` outright — a day changes through its own route (`/skill/add-a-day.md`), never as a side effect of shortening a list here. Concurrent edits use `If-Match: <etag>` from the last `GET`; a stale or missing one on a create retry answers `409 stale_document` carrying the document as it actually stands.

## Renaming a trip

A trip's id was chosen wrong (a typo, a duplicated year) and there is a real door back:

```http
POST /api/v2/{user}/trips/{trip}/rename
Content-Type: application/json
Authorization: Bearer fs_agent_…

{"id": "the-new-id"}
```

Owner only, and refused when the new id is not a lowercase-and-hyphens slug (60 characters or fewer), is the trip's own current id, or is already another trip's id. Nothing is lost: every database row a `trip_id` column names moves with it, and the old id keeps answering — `GET`/`PATCH`/`DELETE` at it come back `308` to the new address, and so does the reader-facing trip page.

## Drawing the party

A figure is a reusable entry in the journal's own library, not something typed inline on a trip:

```http
GET  /api/v2/{user}/figures/presets           — the whole vocabulary, and starting points
GET  /api/v2/{user}/figures/preview?figure={…}  — draw one figure as an SVG before it is written
PUT  /api/v2/{user}/figures/{id}                — create at a client-chosen id, or replace with `If-Match: <etag>` from the last GET
```

Ask how somebody wants to be drawn — never infer it from a name, a country or a photograph — and show them the preview before it is written. A trip then names which figures walk it (`figures.mode`); it never repeats their description.
