API quickstart
Book a stay over HTTP in five steps, then each call in curl, TypeScript and Python. Every example is a file checked against the contract.
Every call sends FORTRIP_TOKEN, an access token of the user who signs in. There is no test environment: searching, getting offers and creating an order are free, an unpaid order expires on its own, and any payment is real. See Testing your integration.
The TypeScript examples use fetch on Node.js, and the Python examples use httpx.
End to end in five steps
You need curl and jq. Each call saves its answer to a file, and the next call takes its id from that file.
1. Sign in
Run this, then approve the sign-in in your browser. It saves the sign-in script as device-login.sh and runs it, keeping the access token and the refresh token in token.json. Sign in explains the flow.
umask 077 # token.json and the files after it are readable by you only
cat > device-login.sh <<'SH'
# Sign in with the OAuth device flow and print the tokens.
# Needs curl and jq. Prints access_token and refresh_token as JSON: export the
# access token as FORTRIP_TOKEN, and keep the refresh token to get a new one.
set -euo pipefail
config=$(curl -sS --fail-with-body --max-time 60 "https://api.fortrip.ai/v1/auth/config")
client_id=$(jq -r .clientId <<<"$config")
token_endpoint=$(jq -r .tokenEndpoint <<<"$config")
device=$(curl -sS --fail-with-body --max-time 60 "$(jq -r .deviceAuthorizationEndpoint <<<"$config")" \
-d client_id="$client_id" \
--data-urlencode scope="$(jq -r .scopes <<<"$config")")
device_code=$(jq -r '.device_code // empty' <<<"$device")
if [ -z "$device_code" ]; then
echo "No device_code in the answer: $device" >&2
exit 1
fi
echo "Open $(jq -r '.verification_uri_complete // .verification_uri' <<<"$device") and approve the sign-in." >&2
interval=$(jq -r '.interval // 5' <<<"$device")
deadline=$((SECONDS + $(jq -r '.expires_in // 600' <<<"$device")))
while [ "$SECONDS" -lt "$deadline" ]; do
sleep "$interval"
token=$(curl -sS --max-time 60 "$token_endpoint" \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d device_code="$device_code" \
-d client_id="$client_id")
case "$(jq -r '.error // empty' <<<"$token")" in
"") jq '{access_token, refresh_token}' <<<"$token"; exit 0 ;;
authorization_pending) ;;
slow_down) interval=$((interval + 5)) ;;
*) echo "$token" >&2; exit 1 ;;
esac
done
echo "The sign-in expired before it was approved. Run this again." >&2
exit 1
SH
bash device-login.sh > token.json
export FORTRIP_TOKEN=$(jq -r .access_token token.json)2. Search
Search a city for the stay, and take the first hotel's hotelId.
curl -sS --fail-with-body --max-time 60 -X POST "https://api.fortrip.ai/v1/hotels/search" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- > search.json <<JSON
{
"destination": {
"kind": "place_name",
"name": "Kyoto",
"countryCode": "JP"
},
"stay": {
"checkIn": "2027-03-10",
"checkOut": "2027-03-12",
"rooms": [
{
"adults": 2,
"childrenAges": []
}
]
},
"filters": {
"amenities": [
"wifi"
]
},
"sort": "price_asc"
}
JSON
hotel=$(jq -re '.data[0].hotelId' search.json) || { echo "No hotel found: search another place or other dates." >&2; false; }The answer, abridged:
{
"destination": {
"place": {
"placeId": "plc_1857910",
"name": "Kyoto"
}
},
"data": [
{
"hotelId": "htl_Qm3xK9vT2pLw7Hn4Rb8Z",
"name": "Kamo Lantern Hotel",
"price": {
"fromPerRoomStay": {
"amount": "123.00",
"currency": "USD"
}
}
}
],
"nextCursor": "c_kXy7Qp2LmN4"
}3. Get offers
Get live offers for that hotel, and take the first offer's offerId.
curl -sS --fail-with-body --max-time 60 -X POST "https://api.fortrip.ai/v1/hotels/offers" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- > offers.json <<JSON
{
"hotelIds": [
"${hotel:?no hotelId: run step 2 first}"
],
"stay": {
"checkIn": "2027-03-10",
"checkOut": "2027-03-12",
"rooms": [
{
"adults": 2,
"childrenAges": []
}
]
},
"filters": {
"requireFreeCancellation": true
}
}
JSON
offer=$(jq -re '[.hotels[].offers[]][0].offerId' offers.json) || { echo "No offer matches: pick another hotel from step 2." >&2; false; }The answer, abridged:
{
"hotels": [
{
"hotel": {
"hotelId": "htl_Qm3xK9vT2pLw7Hn4Rb8Z"
},
"offers": [
{
"offerId": "off_Bz6yN1cD5fGh8JkM2pQs",
"expiresAt": "2027-02-20T08:30:00Z",
"price": {
"total": {
"amount": "132.40",
"currency": "USD"
}
},
"cancellation": {
"status": "free",
"freeCancellationEndsAt": "2027-03-08T15:00:00Z"
},
"confirmation": {
"mode": "instant"
}
}
]
}
]
}4. Create the order
Order the offer with the guests and a contact. Nothing is charged. It can take a couple of minutes, so allow at least three.
curl -sS --fail-with-body --max-time 180 -X POST "https://api.fortrip.ai/v1/orders" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- > order.json <<JSON
{
"contact": {
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"phone": "+14155550132"
},
"items": [
{
"offerId": "${offer:?no offerId: run step 3 first}",
"hotel": {
"rooms": [
{
"guests": [
{
"firstName": "Ada",
"lastName": "Lovelace"
},
{
"firstName": "Charles",
"lastName": "Babbage"
}
]
}
]
}
}
]
}
JSONThe answer, abridged:
{
"orderId": "ord_Wr4tY7uI0oPa3SdF6gHj",
"reference": "FT-8KQ2MX",
"status": "awaiting_payment",
"price": {
"total": {
"amount": "132.40",
"currency": "USD"
}
},
"payment": {
"url": "https://fortrip.ai/pay/ord_Wr4tY7uI0oPa3SdF6gHj?s=cs_live_b1Q9xT4mK7pW2rZ8vN3c",
"expiresAt": "2027-02-20T11:49:12Z"
}
}If the price or terms changed, the answer is 409 offer_changed instead, and order.json holds the error. Show the user error.details.changes; if they agree, order error.details.offer.offerId the same way. See When the offer changed.
5. Give the user the payment link
Show the user the total and every term, and wait for a clear yes. Then give them payment.url, where they pay.
jq -r .payment.url order.jsonhttps://fortrip.ai/pay/ord_Wr4tY7uI0oPa3SdF6gHj?s=cs_live_b1Q9xT4mK7pW2rZ8vN3cSign in
This script runs the OAuth device flow with the settings GET /v1/auth/config returns, and prints the access token and the refresh token. Authentication explains the flow and when to use another.
#!/usr/bin/env bash
# Sign in with the OAuth device flow and print the tokens.
# Needs curl and jq. Prints access_token and refresh_token as JSON: export the
# access token as FORTRIP_TOKEN, and keep the refresh token to get a new one.
set -euo pipefail
config=$(curl -sS --fail-with-body --max-time 60 "https://api.fortrip.ai/v1/auth/config")
client_id=$(jq -r .clientId <<<"$config")
token_endpoint=$(jq -r .tokenEndpoint <<<"$config")
device=$(curl -sS --fail-with-body --max-time 60 "$(jq -r .deviceAuthorizationEndpoint <<<"$config")" \
-d client_id="$client_id" \
--data-urlencode scope="$(jq -r .scopes <<<"$config")")
device_code=$(jq -r '.device_code // empty' <<<"$device")
if [ -z "$device_code" ]; then
echo "No device_code in the answer: $device" >&2
exit 1
fi
echo "Open $(jq -r '.verification_uri_complete // .verification_uri' <<<"$device") and approve the sign-in." >&2
interval=$(jq -r '.interval // 5' <<<"$device")
deadline=$((SECONDS + $(jq -r '.expires_in // 600' <<<"$device")))
while [ "$SECONDS" -lt "$deadline" ]; do
sleep "$interval"
token=$(curl -sS --max-time 60 "$token_endpoint" \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d device_code="$device_code" \
-d client_id="$client_id")
case "$(jq -r '.error // empty' <<<"$token")" in
"") jq '{access_token, refresh_token}' <<<"$token"; exit 0 ;;
authorization_pending) ;;
slow_down) interval=$((interval + 5)) ;;
*) echo "$token" >&2; exit 1 ;;
esac
done
echo "The sign-in expired before it was approved. Run this again." >&2
exit 1GET /v1/auth/config answers, abridged. The values are examples: read them from it each time, as the script does.
{
"clientId": "ExampleClientId0",
"scopes": "openid profile email offline_access",
"deviceAuthorizationEndpoint": "https://clerk.fortrip.ai/oauth/device_authorization",
"tokenEndpoint": "https://clerk.fortrip.ai/oauth/token"
}Refresh a token
The access token is short-lived. Get a new one with the refresh token, at the same token endpoint. Each refresh returns a new refresh token; keep only the newest (why).
umask 077 # the new token.json is readable by you only
config=$(curl -sS --fail-with-body "https://api.fortrip.ai/v1/auth/config")
refresh_token=$(jq -r .refresh_token token.json)
curl -sS --fail-with-body "$(jq -r .tokenEndpoint <<<"$config")" \
-d grant_type=refresh_token \
-d refresh_token="$refresh_token" \
-d client_id="$(jq -r .clientId <<<"$config")" > token.new.json && mv token.new.json token.json
export FORTRIP_TOKEN=$(jq -r .access_token token.json)Search
#!/usr/bin/env bash
# Search hotels in a city.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 60 -X POST "https://api.fortrip.ai/v1/hotels/search" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"destination": {
"kind": "place_name",
"name": "Kyoto",
"countryCode": "JP"
},
"stay": {
"checkIn": "2027-03-10",
"checkOut": "2027-03-12",
"rooms": [
{
"adults": 2,
"childrenAges": []
}
]
},
"filters": {
"amenities": [
"wifi"
]
},
"sort": "price_asc"
}
JSONIts answer is under step 2.
Offers
#!/usr/bin/env bash
# Get live offers for a hotel.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 60 -X POST "https://api.fortrip.ai/v1/hotels/offers" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"hotelIds": [
"htl_Qm3xK9vT2pLw7Hn4Rb8Z"
],
"stay": {
"checkIn": "2027-03-10",
"checkOut": "2027-03-12",
"rooms": [
{
"adults": 2,
"childrenAges": []
}
]
},
"filters": {
"requireFreeCancellation": true
}
}
JSONIts answer is under step 3.
Order
Ordering charges nothing. Show the user the total and every term, and wait for a clear yes before anyone pays. The order can take a couple of minutes; a call that timed out is safe to send again. See Orders and payment.
#!/usr/bin/env bash
# Create an order from an offer. Nothing is charged until it is paid.
# It can take a couple of minutes. A call that timed out is safe to send again:
# https://docs.fortrip.ai/api#retries
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 180 -X POST "https://api.fortrip.ai/v1/orders" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"contact": {
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"phone": "+14155550132"
},
"items": [
{
"offerId": "off_Bz6yN1cD5fGh8JkM2pQs",
"hotel": {
"rooms": [
{
"guests": [
{
"firstName": "Ada",
"lastName": "Lovelace"
},
{
"firstName": "Charles",
"lastName": "Babbage"
}
]
}
]
}
}
]
}
JSONIts answer is under step 4.
Pay
An order is paid one way only. Orders and payment says which ways an order offers and when each applies.
Payment link
The payment link needs no request: give the user the order's payment.url. Read it from the order.
#!/usr/bin/env bash
# Read an order, and print its payment link when it awaits payment.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
link=$(curl -sS --fail-with-body --max-time 60 "https://api.fortrip.ai/v1/orders/ord_Wr4tY7uI0oPa3SdF6gHj" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
| jq -r '.payment.url // empty')
if [ -z "$link" ]; then
echo "The order has no payment link: it is not awaiting payment, or read it again." >&2
exit 1
fi
echo "$link"The answer, abridged:
{
"orderId": "ord_Wr4tY7uI0oPa3SdF6gHj",
"status": "awaiting_payment",
"payment": {
"status": "unpaid",
"url": "https://fortrip.ai/pay/ord_Wr4tY7uI0oPa3SdF6gHj?s=cs_live_b1Q9xT4mK7pW2rZ8vN3c"
}
}Link token
Only for agents that can request a Link token, a shared payment token from the user's Stripe Link wallet.
#!/usr/bin/env bash
# Pay an order with a Link token.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 180 -X POST "https://api.fortrip.ai/v1/orders/ord_Wr4tY7uI0oPa3SdF6gHj/pay" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"sharedPaymentToken": "spt_ExampleTokenFromTheUsersLinkWallet",
"total": {
"amount": "132.40",
"currency": "USD"
}
}
JSONThe answer, abridged:
{
"orderId": "ord_Wr4tY7uI0oPa3SdF6gHj",
"status": "confirming",
"payment": {
"status": "authorized",
"url": null
}
}Alipay or WeChat Pay
#!/usr/bin/env bash
# Pay an order with Alipay or WeChat Pay: opens the payment.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 180 -X POST "https://api.fortrip.ai/v1/orders/ord_Wr4tY7uI0oPa3SdF6gHj/pay" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"method": "alipay_wechat_pay",
"total": {
"amount": "132.40",
"currency": "USD"
}
}
JSONThe answer, abridged:
{
"orderId": "ord_Wr4tY7uI0oPa3SdF6gHj",
"status": "awaiting_payment",
"payment": {
"wallets": {
"open": true,
"current": "wechat_pay",
"wechatPay": {
"qrData": "weixin://wxpay/bizpayurl?pr=Q7nT2kX",
"qrImageUrl": "https://qr.stripe.com/YWNjdF9FeGFtcGxlV2VDaGF0UGF5UXI.png",
"url": "https://fortrip.ai/pay/ord_Wr4tY7uI0oPa3SdF6gHj/wechat?t=wl_4mZ8qR2xN7cV1bK6"
},
"alipay": {
"url": "https://fortrip.ai/pay/ord_Wr4tY7uI0oPa3SdF6gHj/alipay?t=wl_4mZ8qR2xN7cV1bK6"
}
}
}
}List orders
#!/usr/bin/env bash
# List your orders, newest first.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 60 "https://api.fortrip.ai/v1/orders?limit=10" \
-H "Authorization: Bearer $FORTRIP_TOKEN"The answer, abridged:
{
"data": [
{
"orderId": "ord_Wr4tY7uI0oPa3SdF6gHj",
"reference": "FT-8KQ2MX",
"status": "awaiting_payment"
}
],
"nextCursor": null
}Cancel
Cancel only while the order's cancel.available is true, with maximumPenalty the number in cancel.penalty.amount that the user agreed to.
#!/usr/bin/env bash
# Cancel an order under its terms, accepting no penalty.
# Needs FORTRIP_TOKEN, a Fortrip access token (see Sign in above).
set -euo pipefail
curl -sS --fail-with-body --max-time 60 -X POST "https://api.fortrip.ai/v1/orders/ord_Wr4tY7uI0oPa3SdF6gHj/cancel" \
-H "Authorization: Bearer $FORTRIP_TOKEN" \
-H "Content-Type: application/json" \
--data @- <<'JSON'
{
"maximumPenalty": 0
}
JSONThe answer, abridged:
{
"orderId": "ord_Wr4tY7uI0oPa3SdF6gHj",
"status": "cancelling",
"cancel": {
"available": false,
"reason": "in_progress",
"penalty": null,
"refund": null
}
}