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.netlify.app/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.netlify.app/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.netlify.app/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.netlify.app/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.netlify.app/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.netlify.app/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.netlify.app/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.netlify.app/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.netlify.app/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 trip concierge: conversational research and recommendations over the live catalogue and the web.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| POST | /api/v1/lee | Say something to Lee, the trip concierge 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 |
| GET | /api/v1/lee/{id} | Read a conversation with Lee Messages, cards and links in a thread. 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 |
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}/review | Review a completed booking One review per booking; goes into moderation before it appears 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 |
Signed-in customer conveniences.
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| 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.