Everything the interface can do is reachable here — that's the point of it. Field names and types mirror the board exactly, so a plan drafted in a conversation can be posted without a translation layer.
Create a token under Settings and send it as a bearer token. It carries your access — the trips you own or were invited to, and nothing more.
curl https://planwithtriply.com/api/trips \ -H "Authorization: Bearer triply_…"
An OAuth access token works everywhere a personal one does — see OAuth below. Either way the caller acts as one person, with exactly that person's access.
trip.ly is its own OAuth 2.1 authorization server, so a client can connect itself: it registers, opens a browser, and the person approves it — nobody handles a credential by hand. Discovery starts at the resource metadata, which /api/mcp also names in the WWW-Authenticate header of its 401.
GET /.well-known/oauth-protected-resource GET /.well-known/oauth-authorization-server POST /api/oauth/register dynamic client registration (RFC 7591) GET /oauth/authorize consent, then a code POST /api/oauth/token authorization_code | refresh_token POST /api/oauth/revoke RFC 7009
PKCE with S256 is required, code is the only response type, and there is one scope — triply — because there is one thing to grant. Connected apps are listed and revocable under Settings.
Trip { id, title, activeCityId, dayStartMin, shareToken, revision, cities[] }
City { id, key, title, dayStartMin, position, columns[] }
Column { id, key, title, timed, date, position, items[] }
Item { id, title, time, dayOffset, durationMin, blurb, tags[],
stops[], travelMode, isPlace, done, mapsUrl, position }time — 24-hour "HH:MM", or null when unscheduled. A null time puts the card in the day's unscheduled tray.dayOffset — midnights past the column's own date. Use 1 for anything after midnight, so a 00:47 arrival sorts after a 19:33 departure instead of before it.durationMin — optional; renders the card as a block rather than a point.stops — an ordered route through the activity, as plain place names (["Whitehall","Trafalgar Square","Kingly Court"]). Order is the order they're visited and a repeat is a repeat, so unlike tags nothing is sorted or de-duplicated. The city is appended when a stop doesn't already name it, so short names are enough. travelMode is walking unless set to transit, driving or bicycling.isPlace — puts a card with no stops on the day map, pinned at its title. null (the default) means no pin; a card with stops is pinned regardless.done — true once the place has been visited. A done card stays on the board but is left off the day map: no pin, and not a stop on the day's route or its Google Maps link. Defaults to false.mapsUrl — read-only, derived from stops on the way out: a Google Maps link for the whole route, or null when there are no stops. Sending it back changes nothing. Writes return it too, so a client that has just set a route never has to build a Maps URL itself.dayStartMin — minutes past midnight where the day's time axis opens: 480 is 08:00 and is the default, 600 is 10:00. On the half hour, 0–720. A trip always has one; a city's is null until it overrides the trip's. Nothing can hide behind it — the axis still grows upward to hold an earlier item, so it only decides where an empty day begins.key — a stable handle unique within its parent. Cities and columns can be addressed by id or key, so /cities/london works. The key backlog is reserved and cannot be deleted.| GET | /api/trips | Every trip you belong to. |
| POST | /api/trips | Create a trip. Body: { title } |
| GET | /api/trips/:tripId | The whole board as nested JSON. |
| PATCH | /api/trips/:tripId | Body: { title?, activeCityId?, dayStartMin? } |
| DELETE | /api/trips/:tripId | Owner only. |
| GET | /api/trips/:tripId/revision | Cheap change check for polling. |
The endpoint that matters most — create whole days in one call.
| POST | /api/trips/:tripId/import | Body: { cities: City[], replace?: boolean }. Returns the new board. |
| GET | /api/trips/:tripId/cities | All cities with their columns. |
| POST | /api/trips/:tripId/cities | Create one. Accepts a bare title or a full nested city. |
| GET | /api/trips/:tripId/cities/:city | By id or handle. |
| PATCH | /api/trips/:tripId/cities/:city | Body: { title?, position?, dayStartMin? } |
| DELETE | /api/trips/:tripId/cities/:city | Cascades. |
Tags belong to a city — the same name in another city is untouched.
| PATCH | /api/trips/:tripId/cities/:city/tags | Body: { tag, name }. 409 if the new name is already used in the city. |
| DELETE | /api/trips/:tripId/cities/:city/tags?tag=… | Strip the tag off every card in the city. |
| POST | /api/trips/:tripId/cities/:city/columns | Body: { title, timed?, date?, items? } |
| PATCH | /api/trips/:tripId/columns/:column | Body: { title?, timed?, date?, position? } |
| DELETE | /api/trips/:tripId/columns/:column | The "backlog" column is reserved — returns 409. |
| POST | /api/trips/:tripId/columns/:column/reorder | Body: { order: string[] } |
| POST | /api/trips/:tripId/columns/:column/items | Body: { title?, time?, dayOffset?, durationMin?, blurb?, tags?, stops?, travelMode?, isPlace?, done? } |
| PATCH | /api/trips/:tripId/items/:itemId | Same fields, all optional. |
| DELETE | /api/trips/:tripId/items/:itemId | |
| POST | /api/trips/:tripId/items/:itemId/move | Body: { columnId, time?, dayOffset?, order? } |
| GET | /api/trips/:tripId/members | Members plus pending invites. |
| POST | /api/trips/:tripId/members | Body: { email }. Owner only. |
| DELETE | /api/trips/:tripId/members?userId=… | ?email=… | Remove a member, or withdraw an invite. |
| POST | /api/trips/:tripId/share | Reset the join link. Owner only. |
curl -X POST https://planwithtriply.com/api/trips/TRIP_ID/import \
-H "Authorization: Bearer triply_…" \
-H "content-type: application/json" \
-d '{
"cities": [{
"title": "Barcelona",
"columns": [{
"id": "fri16",
"title": "Fri 16",
"timed": true,
"date": "2026-10-16",
"items": [
{ "title": "Sagrada Família", "time": "10:00",
"blurb": "Book the tower slot.", "tags": ["landmark"] },
{ "title": "Dinner in Gràcia", "time": "21:00", "tags": ["food"] }
]
}]
}]
}'Every city gets a Backlog automatically if the payload omits one. Pass "replace": true to clear existing cities first — destructive, and off by default.
The same operations are exposed as MCP tools at /api/mcp, so Claude can edit the board directly instead of a human relaying requests. The URL is the whole configuration — the client discovers the OAuth endpoints from it and sends you off to sign in. Step-by-step setup is on Settings.
claude mcp add --transport http triply https://planwithtriply.com/api/mcp
Tools: list_trips, get_board, get_city, create_trip, update_trip, delete_trip, set_tag_style, rename_tag, delete_tag, import_cities, create_city, update_city, delete_city, create_column, update_column, delete_column, create_item, create_items, update_item, update_items, move_item, move_items, delete_item, delete_items.
The plural item tools — create_items, update_items, move_items, delete_items — take an array and do the same work as their singular counterparts in one round trip, so an agent editing a whole day's worth of activities doesn't need one call per card.
Arguments are validated against the same schemas as the REST API, so a bad time is rejected rather than quietly dropped, and an unrecognised argument is an error rather than a no-op. Inviting people and rotating the share link stay off the tool surface — they're owner decisions, made in the app.
Failures return a JSON body with error, message and sometimes details. A trip you aren't a member of returns 404 rather than 403, so the API never confirms that an id exists to someone with no access to it.