A small, boring JSON API over HTTPS. Search the catalogue and read live availability without any credentials; add an API key to read your manifest, put walk-ups on a departure and check guests in at the door.
https://tourlies.com/api/v1. Everything is JSON in, JSON out, UTF-8.totalCents: 6200 is $62.00 in the booking’s currency.localDate, localTime), with the IANA timezone alongside them. startsAt is the same instant in UTC.TRL-7K3M9Q); listing endpoints accept the id or the slug.Catalogue endpoints need nothing at all. Everything that touches a booking needs one of two credentials:
tourlies_session) — what the Tourlies website itself uses. Partners can’t mint these; they exist so first-party checkout and the supplier portal can share the same API.Send it as a bearer token (or as X-Api-Key if that suits your HTTP client better):
Authorization: Bearer tk_live_a7k3m9qp_x8Fv2…
X-Api-Key: tk_live_a7k3m9qp_x8Fv2…A key is shown once, when you create it — we store only a hash, so a lost key is replaced, never recovered. Keys are scoped to your operator account: they can only see and change your own listings and bookings. Revoking takes effect immediately, so rotate as often as you like. Never ship a key in browser or mobile code.
| Scope | Grants |
|---|---|
catalog:read | Listings, destinations, categories and availability. Rarely needed — the catalogue is public anyway. |
bookings:read | Read your own booking manifest and any single booking on your listings. |
bookings:write | Create bookings on your own listings and redeem vouchers at the door. |
A key missing the scope an endpoint needs gets 403 with code FORBIDDEN; a missing, unknown or revoked key gets 401 with code UNAUTHENTICATED.
Every failure has the same shape, and the message is always safe to show to an end user:
{
"error": {
"code": "CAPACITY",
"message": "Only 2 seats left for that time."
}
}Validation failures add an issues array pointing at the offending fields.
| Status | Typical codes | Meaning |
|---|---|---|
| 400 | VALIDATION, UNKNOWN_CITY | Malformed request or a filter value we don’t recognise. |
| 401 | UNAUTHENTICATED | No credential, or the key is unknown or revoked. |
| 403 | FORBIDDEN | Valid credential, wrong scope or someone else’s data. |
| 404 | NOT_FOUND | No such listing, booking, city or key. |
| 409 | CAPACITY, UNAVAILABLE, STATE | Sold out, past cutoff, or the booking isn’t in a state that allows this. |
| 422 | PROMO, REDEEM, TICKET | Well-formed but refused — bad promo code, already-redeemed voucher. |
| 429 | RATE_LIMITED | Slow down and retry. |
| 500 | INTERNAL | Ours. Retry; if it persists, tell us the time and endpoint. |
/experiences, /experiences/{id}, /cities, /categories, /suggest): 120 requests per minute per IP.POST /bookings/hold): 20 per minute per IP, since each one reserves real capacity.cache-control: public, max-age=60 and change slowly. Availability is never cached; read it live.Over the line you get 429 with code RATE_LIMITED. Back off and retry.
curl -s 'https://tourlies.com/api/v1/experiences?city=barcelona&freeCancellation=1&sort=rating&limit=5'
# → { "items": [ { "id": "…", "slug": "park-guell-tickets", "title": "…",
# "fromPriceCents": 3400, "currency": "EUR", … } ],
# "total": 37, "limit": 5, "offset": 0 }Filter with q, city, category, minPrice/maxPrice (cents),instant, minRating; page with limit (max 50) and offset. City and category take the slugs from /api/v1/cities and /api/v1/categories.
curl -s 'https://tourlies.com/api/v1/experiences/EXPERIENCE_ID/availability?from=2026-09-01&days=14'
# → { "timezone": "Europe/Madrid",
# "days": [ { "date": "2026-09-01", "available": 12,
# "departures": [ { "time": "10:00", "available": 12,
# "capacity": 20, "closed": false } ] } ] }available is already net of other people’s holds and confirmed bookings, andclosed flips true past the listing’s booking cutoff. Don’t cache it.
This is what the Tourlies website does, and what you’d mirror if you were selling to a consumer. No API key involved; the calls carry the customer’s session or the guest cookie returned by the hold.
# Reserve the seats for a few minutes
curl -s -X POST 'https://tourlies.com/api/v1/bookings/hold' \
-H 'content-type: application/json' \
-d '{"experienceId":"EXPERIENCE_ID","date":"2026-09-01","time":"10:00",
"items":[{"ticketTypeId":"TICKET_TYPE_ID","quantity":2}]}'
# → { "checkoutId": "…", "reference": "TRL-7K3M9Q", "expiresAt": "…" }
# Attach the traveller (and a promo code, if any)
curl -s -X PUT 'https://tourlies.com/api/v1/bookings/CHECKOUT_ID/details' \
-H 'content-type: application/json' \
-d '{"leadName":"Ada Lovelace","leadEmail":"ada@example.com"}'
# Hand off to payment
curl -s -X POST 'https://tourlies.com/api/v1/bookings/CHECKOUT_ID/pay'
# → { "mode": "stripe", "url": "https://checkout.stripe.com/…" }Operators selling their own inventory — walk-ups, phone bookings, your own site — skip payment entirely. One call creates and confirms the booking, because partner volume is invoiced offline. The key’s operator must own the listing, and the listing must be bookable natively on Tourlies.
curl -s -X POST 'https://tourlies.com/api/v1/bookings' \
-H "authorization: Bearer $TOURLIES_KEY" \
-H 'content-type: application/json' \
-d '{"experienceId":"EXPERIENCE_ID","date":"2026-09-01","time":"10:00",
"items":[{"ticketTypeId":"TICKET_TYPE_ID","quantity":2}],
"leadName":"Grace Hopper","leadEmail":"grace@example.com"}'
# → 201 { "booking": { "reference": "TRL-4H2XPQ", "status": "confirmed",
# "totalCents": 6200, "supplierPayoutCents": 5270, … } }The guest gets their confirmation email and voucher automatically. Needs bookings:write.
Scan the QR on the voucher; it contains a signed code. Post it against the booking it belongs to. Redemption is single-use — a second attempt returns 422 with the time of the first.
curl -s -X POST 'https://tourlies.com/api/v1/bookings/TRL-4H2XPQ/redeem' \
-H "authorization: Bearer $TOURLIES_KEY" \
-H 'content-type: application/json' \
-d '{"code":"TRL-4H2XPQ.9f21c0a4d7b3e8f1"}'
# → { "ok": true, "redeemedAt": "2026-09-01T08:02:11.000Z", "booking": { … } }curl -s 'https://tourlies.com/api/v1/supplier/bookings?from=2026-09-01&to=2026-09-07&status=confirmed' \
-H "authorization: Bearer $TOURLIES_KEY"
# → { "items": [ { "reference": "TRL-4H2XPQ", "localDate": "2026-09-01",
# "localTime": "10:00", "guests": 2, "leadName": "Grace Hopper",
# "experienceTitle": "…", "items": [ … ] } ], "total": 18 }Needs bookings:read. Platform-wide keys (not tied to an operator) are refused here — this endpoint is deliberately about your bookings.
Tourlies consumes one webhook of its own — Stripe, at /api/webhooks/stripe — to mark bookings paid. That endpoint is internal plumbing: it is signature-verified against our own Stripe account and is not something partners call or subscribe to.
Partner webhooks are coming soon. When they land you’ll be able to subscribe tobooking.confirmed, booking.cancelled and booking.redeemed with a signed payload. Until then, poll GET /api/v1/supplier/bookings — every minute is plenty, and the response is small.
Generated from the same OpenAPI document we serve at /api/v1/openapi.json, so it can’t drift from the API.
Public listing, destination and availability data.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/categories | Category tree Top-level categories, each with its children. | Public |
| GET | /api/v1/cities | List destinations Every city Tourlies sells in. | Public |
| GET | /api/v1/cities/{slug} | Get one destination Includes `experienceCount` and the cheapest listing price in that city. | Public |
| GET | /api/v1/experiences | Search the catalogue Public, cached for 60s. Returns the same card shape the website's result grid renders. | Public |
| GET | /api/v1/experiences/{id} | Get one listing Accepts either the listing id (UUID) or its slug. Only published listings are visible. | Public |
| GET | /api/v1/experiences/{id}/availability | Departure availability Live seat counts per departure, already net of held and confirmed bookings. Never cached. | Public |
| GET | /api/v1/suggest | Search typeahead Mixed city / category / listing suggestions for a search box. | Public |
AI-composed day-by-day itineraries built from the live catalogue.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| POST | /api/v1/plan | Compose a trip plan The AI trip planner: give it a destination and preferences, get a stored, shareable day-by-day itinerary built from bookable listings plus free-form suggestions. Composed by Claude when the platform has an Anthropic key, by a deterministic composer otherwise (`source` tells you which). Tightly rate limited. | Public |
| GET | /api/v1/plan/{id} | Get a stored trip plan Readable by anyone holding the (unguessable) plan id — plans are meant to be shared. | Public |
Lee, the travel assistant: conversational research, planning, memory and booking over the live catalogue and the web, with a server-enforced confirmation protocol for anything irreversible.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| POST | /api/v1/lee | Say something to Lee, the travel assistant Lee turns a loose wish ("I'm in Austin next weekend and want BBQ") into a researched, bookable shortlist: it searches Tourlies listings, reads their reviews, checks live departures, researches the web for what Tourlies doesn't sell, and can compose a multi-day itinerary. Omit `threadId` to start a conversation; keep the returned thread id to continue it (guests hold it client-side). Add `?stream=1` to receive Server-Sent Events (`data: {type: thread|status|text|cards|actions|done|error, …}`) instead of one JSON body. Answered by Claude with tools when the platform has an Anthropic key, by deterministic catalogue retrieval otherwise (`thread.source`). Tightly rate limited. | Public |
| DELETE | /api/v1/lee/{id} | Delete your own conversation | Session |
| GET | /api/v1/lee/{id} | Read a conversation with Lee A guest thread is readable by anyone holding its unguessable id; once a signed-in traveller has spoken in it, only they can read it. | Public |
| GET | /api/v1/lee/{id}/report | Discovery report for a conversation What the conversation uncovered (listings shown, dates checked, comparisons), what got done (bookings, trips, itineraries, alerts, memories), decisions, open questions, web sources, and next actions — each next action either a link or the exact message to send Lee. Built from the stored conversation without a model call; when an AI key is configured the utility model adds a short narrative from a digest. Cached until new messages arrive; `?fresh=1` regenerates. Same access rule as reading the thread. | Public |
| POST | /api/v1/lee/{id}/report | Email yourself the discovery report | Session |
| POST | /api/v1/lee/confirm | Click Confirm on a confirmation card The confirmation protocol's human step: marks a prepared, irreversible action (booking, cancellation, change, trip delete/share, forget-all) as user-confirmed after verifying the session owns the token. This endpoint is the only way the flag is ever set — nothing typed in chat, and nothing the assistant does, can substitute for it. The UI then sends a chat turn carrying `confirmedToken` so Lee executes the action; tokens are single-use and expire after 10 minutes. | Session |
| GET | /api/v1/lee/threads | Your conversations with Lee | Session |
What Lee remembers about a traveller — profile, episodic memories, never-store list — with edit, export and delete.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/memory | Everything Lee remembers about you Episodic memories with source, confidence and dates; the never-store list (facts you asked Lee to forget); and your traveller profile with per-field provenance. `?format=export` returns a downloadable JSON export that also includes your trips (spec: transparency, export, delete). | Session |
| DELETE | /api/v1/memory/{id} | Forget a memory Deletes the memory and records a suppression so it is never re-inferred; a mirrored profile field is cleared too. `?undo=1` is the chip's Undo right after a write: hard delete, no suppression. | Session |
| PATCH | /api/v1/memory/{id} | Edit a memory's wording | Session |
| GET | /api/v1/profile | Your traveller profile | Session |
| PUT | /api/v1/profile | Set profile fields Body is a map of dotted field path → value (e.g. `{"flights.seat_preference": "aisle", "dietary": ["vegetarian"]}`); `null` clears a field. Everything set here is `user_stated`, which outranks anything Lee inferred. Unknown paths and invalid values are rejected with 400. | Session |
Transport inquiries from the booth funnels (docs/RIDE.md). One public write; the reading is in /admin/inquiries.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/admin/inquiries.csv | Download the ride inquiries as CSV Same filters as /admin/inquiries. Admin session only. | Session |
| POST | /api/v1/inquiries | Booth funnel: ask about transport to next year's event The one write behind the `/ride/{funnel}` pages (docs/RIDE.md): who needs a ride, from where, with how many. Nothing is booked. Idempotent on `clientId`, which the browser mints — a submission queued on bad wifi and sent again answers 200 with the same reference instead of a second row and a second confirmation mail. Consent is a condition of the row: `consent` must be `true`. Rate limited per address. | Public |
First-party product analytics beacon (page views and clicks); everything else is recorded server-side.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| POST | /api/v1/analytics | Site beacon: record page views and clicks Used by the Tourlies web app for first-party product analytics (/admin/analytics). Accepts only `page_view`, `click` and `lee_open`; every other fact (searches, Lee turns, bookings) is recorded server-side by the service that owns it. The `visitorId` the browser generated is set back as a long-lived cookie so later server-side events attribute to the same person. Nothing here stores an IP address or user agent; the browser skips the beacon when Do-Not-Track is on. Rate limited. | Public |
Trip memory documents: destinations, dates, travellers, budget, itinerary, decisions and shared members.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/trips | Your trips Trips you own or were invited to (invites by your email are claimed automatically). | Session |
| POST | /api/v1/trips | Start a trip | Session |
| DELETE | /api/v1/trips/{id} | Delete a trip (owner only) | Session |
| GET | /api/v1/trips/{id} | A trip document | Session |
| PATCH | /api/v1/trips/{id} | Update core trip fields Editors and the owner. Arrays replace the stored value. Itinerary edits, decisions and rejected options are made through Lee or the account page. | Session |
| DELETE | /api/v1/trips/{id}/members | Remove a member (owner only) | Session |
| POST | /api/v1/trips/{id}/members | Share a trip by email The invitee gets an email with a link; membership activates when they sign in with that address. Companions are stored as first name + role only. | Session |
The customer booking flow: hold, details, pay.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| PUT | /api/v1/bookings/{id}/details | Set lead traveller details Step two of the customer flow — also where a promo code is applied, which re-quotes the booking. | Session |
| POST | /api/v1/bookings/{id}/pay | Start payment Step three: returns a Stripe Checkout URL, or `{ mode: "sandbox" }` when Stripe is not configured. | Session |
| POST | /api/v1/bookings/{id}/sandbox-pay | Confirm without a card (demo only) Available only when no Stripe key is configured; 404s otherwise. Used by the demo checkout. | Session |
| POST | /api/v1/bookings/hold | Hold seats for checkout Step one of the customer flow: reserves capacity for a short window and returns a checkout id. Guests receive a scoped cookie granting access to that booking for 48h. | Public |
Reading, cancelling, reviewing and redeeming bookings.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| POST | /api/v1/bookings | Create and confirm a booking (partners) One-shot booking for operators selling their own inventory — walk-ups, phone bookings, other channels. The booking is created and confirmed in a single call with no card involved; partner volume is invoiced offline. The key's supplier must own the listing, and the listing must be `native`. Customers should use `/bookings/hold` followed by `/bookings/{id}/pay`. | API key (bookings:write) |
| GET | /api/v1/bookings/{id} | Get a booking Readable by the customer who owns it (session or guest cookie), or with an API key carrying `bookings:read` whose supplier fulfils the booking. Platform keys may read any booking. | Session or API key (bookings:read) |
| POST | /api/v1/bookings/{id}/cancel | Cancel a booking Refund is computed from the listing's cancellation policy and issued automatically when due. | Session |
| POST | /api/v1/bookings/{id}/redeem | Redeem a voucher at the door Scan the QR, post the code it contains. Single-use: a second redemption returns 422 with the timestamp of the first. | API key (bookings:write) |
| POST | /api/v1/bookings/{id}/reschedule | Move a confirmed booking to another departure Same listing, same tickets, same price and voucher — only the departure changes. Allowed while the listing's free-cancellation window is open; seats on the new departure are checked under the listing lock. Both the traveller and the operator are emailed. | Session |
| POST | /api/v1/bookings/{id}/review | Review a completed booking One review per booking; appears immediately; moderators can take it down, and any reader can report it on the listing. | Session |
| GET | /api/v1/vouchers/{reference}/qr.png | Voucher QR code PNG of the voucher QR. Requires the signed voucher token in `t`, so the reference alone is not enough to fetch it. | Public |
Operator endpoints authenticated with an API key or portal session.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/supplier/api-keys | List your API keys Signed-in operator owners and managers only. Secrets are never returned — only the prefix. | Session |
| POST | /api/v1/supplier/api-keys | Create an API key The raw key is in the response and is never retrievable again. Store it somewhere safe immediately. | Session |
| DELETE | /api/v1/supplier/api-keys/{id} | Revoke an API key Immediate and irreversible. | Session |
| GET | /api/v1/supplier/bookings | Your booking manifest Every booking on your listings, newest departure first. Requires an operator key — platform keys are rejected. | API key (bookings:read) |
| GET | /api/v1/supplier/bookings/export | Download your manifest as CSV Same filters as the portal's bookings list, streamed as `text/csv` with an attachment filename. Portal session only — use `/api/v1/supplier/bookings` for machine-to-machine access. | Session |
| DELETE | /api/v1/supplier/experiences/{id}/images | Remove a listing photo | Session |
| POST | /api/v1/supplier/experiences/{id}/images | Upload listing photos Portal session only (multipart/form-data with one or more `files` parts). Pass `?mode=supplierLogo` to replace the operator logo instead. | Session |
| GET | /api/v1/supplier/payouts/{id}/bookings | The bookings behind one payout statement Every booking a statement settled, with the per-booking commission and refund arithmetic. `?format=csv` returns a spreadsheet-ready file. | Session or API key (bookings:read) |
Signed-in customer conveniences.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/account/export | Download everything held about the signed-in traveller One JSON document: account, bookings with line items, reviews, wishlist, price watches, signed-in devices, trips, the traveller profile and Lee's memories. Served as an attachment; the data-access half of the privacy promise (deletion is a form on /account/settings). | Session |
| POST | /api/v1/reviews/{id}/report | Report a published review to the moderators Anyone may report; signed-in reporters are deduplicated per review. Reasons: offensive, fake, personal_data, off_topic, other. Rate limited. | Public |
| GET | /api/v1/wishlist | Your saved listings | Session |
| POST | /api/v1/wishlist | Save or unsave a listing | Session |
The API describing itself.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/openapi.json | This document The machine-readable description of every endpoint above. | Public |
If the API can’t do what your system needs — bulk availability pushes, a channel-manager style sync, webhooks sooner rather than later — tell us. Mail partners@tourlies.com with what you’re building; the roadmap is mostly written by operators asking for things.