Reference
HTTP API
Fortrip API v1 is the HTTP API behind the MCP tools and the CLI. Each endpoint takes the same input as its MCP tool and answers the same object.
Base URL
https://api.fortrip.ai/v1JSON over HTTPS. Reads are GET with query parameters; everything else is POST with a JSON body. A request with a field the input doesn't define is refused with invalid_request. There is no CORS: call the API from a server, a script or the CLI, not from a web page.
Authentication
Every endpoint except GET /v1/auth/config needs a Fortrip OAuth access token, sent as Authorization: Bearer <token>. Cookies and API keys are not accepted. The tokens are the ones the CLI and MCP clients get when a user signs in to Fortrip; GET /v1/auth/config returns the OAuth settings a client signs in with (the CLI uses the device authorization flow).
A missing or rejected token answers 401 unauthenticated. GET /v1/me returns the signed-in account. Its email is null unless the token has the email scope, and its name unless it has profile.
Endpoints
| Endpoint | Input | Answer | MCP / CLI |
|---|---|---|---|
POST /v1/hotels/searchSearch hotels | JSON body | Search result | search_hotelsCLI |
GET /v1/hotels/{hotelId}Hotel details | Path and query | Hotel | get_hotelCLI |
POST /v1/hotels/offersLive offers | JSON body | Offers result | get_hotel_offersCLI |
POST /v1/ordersCreate order | JSON body | Order | book_hotelCLI |
GET /v1/orders/{orderId}Get order | Path and query | Order | get_orderCLI |
GET /v1/ordersList orders | Path and query | Order list | list_ordersCLI |
GET /v1/meSigned-in account | None | Account | CLI |
GET /v1/auth/configCLI sign-in settings | None | OAuth settings | CLI |
A path parameter is also a field of the MCP tool's input: GET /v1/hotels/{hotelId}?include=rooms&locale=en takes what get_hotel takes as { hotelId, include: "rooms", locale: "en" }. Successful answers are the object itself, with no envelope; failures are the error envelope.
Conventions
- Fields are camelCase. Ids are opaque strings with a prefix:
htl_hotels,off_offers,ord_orders andplc_places. Pass them back exactly as received. - Instants end in
Atand are RFC 3339 in UTC. Dates areyyyy-MM-ddin the hotel's calendar, and times of dayHH:mmin the hotel's time zone, its IANAtimeZone. - Money is
{ amount: "132.40", currency: "USD" }, the amount a decimal string. Money in a request must be USD. Anapproxamount is an estimate in the requesteddisplayCurrency, with the date of its rate; it never changes what is charged. - Coordinates are
{ lat, lng }in WGS84; distances are in meters. - Names with an English and a Chinese form are
name, in the requestedlocale, plusnames: { en, zh }. Other text from the catalogue is{ text, locale }, withlocalenullwhen unknown. Messages the API writes are in English. - Unknown is not no. A list or flag the source didn't state is
null, or comes withknown,listed,disclosureorstatussaying so. An amenity that isn't listed may still exist.
Headers
Request-Id | On every response, and as requestId in an error. Quote it to support. |
RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset | On every response a rate limit counted: the limit per minute, what is left, and seconds until the window resets. See rate limits. |
Retry-After | On every retryable error: seconds to wait. The envelope's retryAfterSeconds says the same. |
Idempotent-Replayed | true when POST /v1/orders answers an order it already created for the same details. |
Pagination
Search results and the order list are pages: { data, nextCursor } for orders, and data with nextCursor alongside the rest of a search result. Pass nextCursor back as cursor; a next page of a search takes only cursor and, optionally, limit. nextCursor is null on the last page. A cursor belongs to its request and user; an expired, edited or foreign one answers 400 cursor_invalid.
Live offers are not a page: they are grouped by hotel, under hotels, with the hotels that have nothing to offer under unavailable and the reason.
Orders and payment
POST /v1/orderschecks the offer with the hotel supplier once. If nothing changed, it answers201with an orderawaiting_paymentand itspayment.url, a Fortrip page where the user pays in USD. Stripe's page may let the user pay in their card's local currency at Stripe's rate (Adaptive Pricing).- If the price or any term changed, it answers
409 offer_changedwith every change and a new offer. No order is made at a price the user hasn't seen. - Sending the same details for the same offer again answers the same order with
200andIdempotent-Replayed: true. Different details for an ordered offer answer409 offer_already_ordered. - Anyone holding
payment.urlcan pay. The MCP tools carry this rule with every answer that has one, asnextStep:Before you give the user an order's payment.url, show them its total and currency, the stay (hotel, room, dates, rooms and guests per room) and every term (meals, cancellation, fees payable at the property), then end your reply by asking them to confirm, and wait for a clear yes. Only after they say yes, give them payment.url exactly as returned.
- Orders can't be changed or cancelled through the API.
GET /v1/orderslists the user's hotel orders, newest first, including those made on the Fortrip website.
Changes within v1
v1 changes only by adding: new fields, endpoints, enum values and error codes. Read responses tolerantly: allow fields you don't know, treat an unknown enum value or error code as its raw string (or by its HTTP status and retryable), and don't fail on them. A breaking change would come as /v2, announced with Deprecation and Sunset headers.