# Wanderlog API: guide for organizing agents

Wanderlog stores vacation photos (R2 + D1) and turns them into published, story-style web pages.
**Your job as the agent:** look at the photos, work out what trips and moments they show, and write
those up as **trips** made of ordered **sections**, each with a short narrative and hand-picked photos.
The website renders whatever you write.

Base URL: `https://wanderlog-1m7.pages.dev`
Auth: every endpoint except `/api/usage`, `/api/health` and `/api/public/*` needs
`Authorization: Bearer <API_TOKEN>`. Image URLs also accept `?token=<API_TOKEN>`.

## Current state
- Photos: **13** (1 hidden), 13 dated, 0 geotagged
- Not yet in any trip: **3**
- AI analysis: done=13
- Date range: 2026-10-15T11:59:42 → 2026-10-16T07:15:15
- Trips: 1 (1 published)

## What's known about each photo
When a photo is uploaded the server extracts EXIF (`taken_at` in camera local time, `tz_offset`, `lat`/`lon`,
`camera`, `lens`, and raw fields under `exif`) and runs a vision model in the background. The model fills in
`caption`, `ocr_text` (text on signs, menus, tickets), `tags`, `scene`, a `place_name` guess when it spots a landmark,
and a 1–5 `rating` (how good a pick it is for a story). These are first drafts, and you should correct them.

## Recommended workflow
1. `GET /api/stats` (or re-read this page) to get the size of the library.
2. If `ai.pending`, `ai.skipped` or `ai.error` > 0, call `POST /api/analyze?limit=20&retry_errors=1` repeatedly until
   `remaining` is 0. If analysis keeps failing (no AI configured), skip this step and inspect the images yourself.
3. `GET /api/photos/clusters?ids=0` to get candidate trips. Photos are split on time gaps (`gap_hours`, default 36)
   and location jumps (`jump_km`, default 150). Tune these: a road trip might need `jump_km=1000`.
   `GET /api/photos/timeline` gives per-day counts and locations.
4. For each cluster, page through `GET /api/photos?from=…&to=…&limit=200`. Read captions, OCR and tags.
   - Use `lat`/`lon` and OCR (station signs, restaurant names, museum tickets) to name places. Save them with
     `PATCH /api/photos` (bulk) using `place_name`.
   - To see a photo yourself, download `GET /api/photos/{id}/image/display` (or `/thumb`) and look at it. Do this
     for anything ambiguous, and always for cover and hero candidates.
   - Mark near-duplicates, blurry shots, screenshots and receipts as `hidden: true` (they stay in the library but are
     left out of stories). Raise `rating` on the best shots.
5. `POST /api/trips` with `{title, subtitle, summary, start_date, end_date, theme, cover_photo_id}`.
6. `PUT /api/trips/{slug}/sections` with the whole story. This replaces all sections atomically, so you can iterate
   safely. Aim for one section per day or per place. Each section gets 3–12 photos, `featured: true` on the 1–2 best,
   a 2–4 sentence `body` (markdown) written in a warm first-person-plural travel-journal voice, and a `layout`.
7. `PATCH /api/trips/{slug}` with `{"published": true}`. The page appears at `/trip?slug={slug}`.

## Endpoints

### Library
| Method | Path | Notes |
|---|---|---|
| GET | /api/usage | This document (markdown). `?format=json` returns a machine-readable endpoint list. |
| GET | /api/stats | Counts shown above, as JSON. |
| POST | /api/photos | multipart: `file` (required), `display` (~2048px), `thumb` (~512px), optional `width`, `height`, `taken_at`. Deduped by SHA-256. |
| GET | /api/photos | Filters: `from`, `to` (ISO date/time), `q` (text search over caption/ocr/tags/place/filename), `scene`, `ai_status`, `unassigned=1`, `trip={slug}`, `has_gps=0/1`, `undated=1`, `min_rating`, `bbox=minLon,minLat,maxLon,maxLat`, `ids=a,b,c`, `hidden=0/1/all` (default 0). Paging: `limit` (≤500), `offset`. `sort=taken_at` (default) / `created_at` (newest uploads first) / `rating`. `fields=full` includes EXIF and raw AI output. |
| GET | /api/photos/clusters | `gap_hours`, `jump_km`, `unassigned=1`, `ids=0` (omit the photo id lists). |
| GET | /api/photos/timeline | Per-day counts, centroid, places and scenes. |
| GET | /api/photos/{id} | Full detail plus `used_in` (the trips and sections that use it). |
| GET | /api/photos/{id}/image/{thumb\|display\|original} | Image bytes. |
| PATCH | /api/photos/{id} | Editable: `caption, ocr_text, scene, place_name, title, notes, rating (0-5), hidden, taken_at, lat, lon, tags[]`. |
| PATCH | /api/photos | Bulk: `[{"id": "...", "place_name": "...", "hidden": true}, ...]` (≤500). |
| PUT | /api/photos/{id}/variants | multipart `display`/`thumb` to add resized copies. |
| DELETE | /api/photos/{id} | Removes the photo from R2, D1 and any sections. |
| POST | /api/photos/{id}/analyze | Re-runs the vision model synchronously. |
| POST | /api/analyze | Works through the pending + skipped backlog: `limit` (default 10, max 50), `retry_errors=1`. |

### Stories
| Method | Path | Notes |
|---|---|---|
| GET | /api/trips | All trips, including drafts, with section and photo counts. |
| POST | /api/trips | `{title, slug?, subtitle?, summary?, start_date?, end_date?, cover_photo_id?, theme?, published?}` |
| GET | /api/trips/{slug} | The full story (draft or published). |
| PATCH | /api/trips/{slug} | Any trip field above. Set `published` to true or false to publish or unpublish. |
| PUT | /api/trips/{slug}/sections | `{"sections": [{title, body, date, place_name, lat, lon, layout, photos: ["id" \| {id, caption?, featured?}]}]}` |
| DELETE | /api/trips/{slug} | Deletes the trip. Photos are kept. |

### Public (no auth, published trips only)
| GET | /api/public/trips | List of published trips. |
| GET | /api/public/trips/{slug} | The story JSON the website renders. |

## Vocabulary
- `theme`: sand | ocean | forest | night | city (sets the page's colors; pick one that matches the trip's mood)
- `layout`: `hero`, `grid`, `mosaic`, `carousel`, `single`, `map`
  - `hero`: one big full-bleed image (the first featured photo) with the text over it. Good for openers.
  - `grid`: an even grid. `mosaic`: a masonry layout that mixes portrait and landscape well.
  - `carousel`: horizontal scroll, good for sequences. `single`: one photo with a long caption.
  - `map`: the section's photos plotted on a map (needs geotagged photos).
- `scene` values from the model: beach, mountain, city, street, food, interior, landmark, nature, water, transport, people, night, document, other

## Tips
- `taken_at` is the camera's local time. Phones that crossed time zones may be off by a few hours.
  Check `tz_offset` and OCR'd clocks or tickets before splitting days.
- `scene=document` photos (tickets, maps, menus) rarely belong in a story, but their OCR is great for naming places.
- Prefer variety within a section: wide, detail, people, food.
- Keep the agent context small by using list endpoints (compact) and fetching `fields=full` only when you need it.
