# Vishlist API — Agent Manual Vishlist stores places a user wants to visit (restaurants, trails, parks, activities). This document is the complete API reference for agents. Base URL: https://vishlist.my/api/v1 ## Authentication Every request needs a bearer token (issued by the user in Settings → API tokens): Authorization: Bearer 401 means your token is missing, revoked, or wrong. ## Conventions — read this first - YOU own location resolution. You have the conversational context to disambiguate ("Cambridge UK" vs "Cambridge MA"), so: - When CREATING a place, supply `lat`/`lng` yourself. Your coordinates are authoritative. If you omit them but give an `address`, the server geocodes it as a fallback (async; may fail). - When QUERYING by location, resolve the place name to coordinates first and pass `lat`/`lng`. There is no free-text location parameter. - `tags` are for filterable facts that apply to many places (e.g. "halal"). `properties` is a free-form JSON object for odd one-off facts (e.g. {"hygiene_rating": "5/5", "phone": "…"}). Never invent new top-level fields. - On 422, the `errors` object has field-level messages. Fix and retry. - On 409, the response lists likely `duplicates`. Prefer PATCHing the existing place; only retry with `"force": true` if it's genuinely a different place. ## Enums (live values from this server) - kind: restaurant, cafe, hiking_trail, park, activity, other - status: wishlist, visited, closed - link kind: tiktok, instagram, website, delivery, map, other - geocode_status: pending, ok, failed, manual ## Create a place POST /api/v1/places Content-Type: application/json { "place": { "name": "Kabul Korner", "kind": "restaurant", "address": "262 High Road, Harrow, London HA3 7BB", "lat": 51.5934, "lng": -0.3305, "notes": "First to bring Afghan street food to London.", "cuisine": "Afghan street food", "tags": ["halal", "afghan"], "properties": { "hygiene_rating": "unknown" }, "source": { "platform": "tiktok", "handle": "@jules.mide", "url": "https://..." }, "links": [ { "kind": "instagram", "url": "https://instagram.com/kabulkorner" }, { "kind": "delivery", "label": "Totalee Halal", "url": "https://..." } ] } } Multi-branch places: instead of flat address/lat/lng, pass `"locations": [{ "label": "Stratford branch", "address": "…", "lat": …, "lng": … }, …]`. The first location is marked primary unless you set `"primary": true` on another. Responses: - 201 → the created place (see Place JSON below) - 422 → { "errors": { "name": ["can't be blank"] } } — fix fields, retry - 409 → { "duplicates": [ …places… ], "hint": "…" } — a similar name exists nearby. PATCH the existing place, or retry the same POST with `"place": { …, "force": true }` to create anyway. ## Query places GET /api/v1/places — all query params optional, filters combine (AND): - lat, lng, radius_km (default 25) — geo search. Matches any branch of a place; results include `distance_km` to the closest branch and are sorted nearest-first. Example: ?lat=53.4808&lng=-2.2426&radius_km=30 - kind=restaurant — exact match - status=wishlist — exact match - tags=halal,afghan — comma-separated, place must have ALL listed tags - cuisine=afghan — case-insensitive substring match - q=fried+chicken — text search over name, notes, cuisine, tags, properties - sort=name — with geo search, sort alphabetically instead of by distance - page=2 — pagination (50 per page). Response: { "places": […], "total": n, "page": n, "per_page": 50 } Examples: GET /api/v1/places?lat=53.4808&lng=-2.2426&radius_km=30&kind=restaurant&tags=halal GET /api/v1/places?q=fried+chicken&status=wishlist GET /api/v1/places?cuisine=afghan ## Read / update / delete GET /api/v1/places/:id PATCH /api/v1/places/:id — same body shape as create; only sent fields change. Sending "locations" or "links" REPLACES the full set. DELETE /api/v1/places/:id — 204 POST /api/v1/places/:id/visit — mark visited today, or pass {"visited_on": "2026-07-01"} ## Discovery endpoints GET /api/v1/tags — { "tags": [{ "name": "halal", "count": 12 }, …] } Check this BEFORE inventing a new tag; reuse existing spellings. GET /api/v1/meta — live enums + distinct cuisines in use. ## Place JSON shape { "id": 1, "name": "…", "kind": "restaurant", "status": "wishlist", "cuisine": "…", "notes": "…", "tags": ["…"], "properties": { … }, "source": { "platform": "tiktok", "handle": "@…", "url": "…" }, "added_on": "2026-06-23", "visited_on": null, "locations": [ { "id": 1, "label": null, "address": "…", "lat": 51.59, "lng": -0.33, "geocode_status": "ok", "primary": true } ], "links": [ { "id": 1, "kind": "instagram", "label": null, "url": "…" } ], "distance_km": 1.2 // only present on geo queries } ## Error semantics summary - 400 — malformed request (missing `place` wrapper, bad JSON) - 401 — bad/missing token - 404 — place doesn't exist or belongs to another user - 409 — duplicate suspected (create only); see `duplicates`, consider PATCH or force - 422 — validation failed; see `errors` per field, fix and retry