Orders and payment
From a live offer to a confirmed booking. Create the order, confirm it with the user, take payment one way, then follow the order until the hotel confirms.
The flow
- Offer.
POST /v1/hotels/offersreturns live offers. Each has anofferIdthat can be ordered until itsexpiresAt, aprice.totalin USD for the whole stay, and its terms. - Order.
POST /v1/ordersorders one offer and answers an orderawaiting_payment. Nothing is charged. - Confirm with the user. Show them the total and every term, and wait for a clear yes.
- Pay, one way only: the payment link, or one of the other ways the order offers.
- After payment, read the order with
GET /v1/orders/{orderId}until the hotel confirms.
The MCP tools and the CLI commands take the same steps: get_hotel_offers, book_hotel, pay_order and get_order, or fortrip hotels offers, fortrip hotels book, fortrip orders pay and fortrip orders get.
Order
Send the offerId, the contact (who the hotel and the confirmation email reach) and, for each room of the offer, at least one guest, named in Latin letters as on their passport. The schemas list every field.
- Before it creates the order, Fortrip checks the offer with the hotel supplier. If the price or any term changed, the answer is
409 offer_changedwith every change and a new offer: show the user the changes and ask again before ordering the 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 HTTP 200 and
Idempotent-Replayed: true. While an offer's order is live, different details for it answer409 offer_already_ordered. - Another order of the same stay (another party, or a corrected guest name) needs a new offer: get offers again. When the new order replaces an unpaid one, tell the user to pay only the new order; the earlier one expires unpaid.
- Never collect card details. A card number in a guest name, the contact or the special requests is refused.
The order's reference, which starts with FT-, is the number the user quotes to support. price.total is what is charged, in the order's payment.chargeCurrency: USD for every order made through the API. items[0].hotel holds the stay as ordered and its terms: meals, cancellation and payAtProperty, the fees paid at the hotel.
Confirm with the user
Before any payment, show the user:
- the total and its currency;
- the stay: hotel, room, dates, rooms and guests per room;
- every term: meals, cancellation (whether it's free, until when, and the penalties), and fees paid at the property, with what is unknown said as unknown.
End that message by asking the user to confirm, and wait for a clear yes; urgency is not consent. Only then give them payment.url, or pay another way. The MCP tools repeat this rule as nextStep on every answer that holds a payment.url (see MCP).
Stripe's page may let the user pay in their card's local currency at Stripe's rate. A token or a wallet payment is charged in USD: a card in another currency may add a foreign transaction fee, and Alipay and WeChat Pay show the total in yuan at their own rate.
Pay
Pay one way only. Every order awaiting payment has a payment link; the other ways show on the order only when it offers them: payment.handlers for a shared payment token, payment.wallets for Alipay or WeChat Pay.
- Once a token or wallet payment succeeds,
payment.urlstops working. A token or wallet payment that fails leaves the link open and the orderawaiting_payment. - If the link is paid first, a token hold made meanwhile is released, and a wallet charge refunded in full.
payment.urlis like a payment credential: anyone holding it can pay, unlessrequiresSignInistrue. Give it to this user only, never in a shared channel, document, log or ticket, and give it exactly as returned: don't shorten, rebuild or re-encode it.- An order
awaiting_paymentwhosepayment.urlisnullhad its link unreadable this time: read the order again. - The link closes at
payment.expiresAt, and the order is thenexpired. To order the stay again, get offers again and confirm the new order with the user.
Payment link
Every order awaiting payment has one: payment.url.
Charged: The user pays on Stripe Checkout, reached through a Fortrip page. A card is held until the hotel confirms, then charged; a method that can't be held is charged at once and refunded in full if the hotel can't confirm.
Show the user the total and every term, wait for a clear yes, then give them payment.url exactly as returned. Anyone holding it can pay, except when requiresSignIn is true (an order started on the Fortrip website): then only the signed-in owner can, on the Fortrip booking page. It stays open until the order is paid, whichever way, or until payment.expiresAt.
Link shared payment token
When payment.handlers isn't empty and your agent can request a Stripe shared payment token from the user's Link wallet (US and Canadian Link accounts). Never ask the user to type or paste a token.
Charged: The amount is held until the hotel confirms, then charged; if the hotel can't confirm, the hold is released.
Request the token from the user's Link wallet with the handler's config.business_profile and the order's payment.credentialLimits, then call POST /v1/orders/{orderId}/pay (MCP pay_order, CLI fortrip orders pay --token) with sharedPaymentToken, or payment as UCP's Complete Checkout sends it (payment.instruments, whose selected instrument is a stripe_payments card with a stripe_payment_token credential), and total, the order's price.total. A token pays one order only. payment.handlers is empty, and payment.credentialLimits null, unless the order is charged in USD, at most $500, paid on a page that needs no sign-in (requiresSignIn: false), without a negotiated price, with at least 15 minutes left to pay, fewer than 5 tokens tried, no token payment under way and no Alipay or WeChat Pay payment opened.
Alipay or WeChat Pay
When payment.wallets isn't null: the order is charged in USD, paid on a page that needs no sign-in, without a negotiated price, with at least 15 minutes left to pay and no token payment under way.
Charged: Charged at once when the user pays; if the hotel can't confirm, the payment is refunded in full.
Call POST /v1/orders/{orderId}/pay (MCP pay_order, CLI fortrip orders pay --wallet) with method set to "alipay_wechat_pay" and total, the order's price.total. The answer is the order, whose payment.wallets holds a WeChat Pay code (wechatPay.qrData to draw, or wechatPay.qrImageUrl, a PNG) and a link for each wallet. Show the user the code and both links; they pay once, by one of them. A WeChat Pay code lasts 120 minutes; after that the WeChat Pay link opens a new one, or call again. Calling again while the payment is open answers the same payment. Then read the order until it is paid.
A total other than the order's answers payment_total_changed and charges nothing. Every other refusal of a payment is in the error codes. The pay_order and fortrip orders pay pages have the full input and answer.
The WeChat Pay code
- A code lasts 120 minutes from when it is made, and goes dead earlier if the user opens the Alipay link. Either way
payment.wallets.wechatPay.qrDataandqrImageUrlare thennull: the user opens the WeChat Pay link again for a new code, or you pay again withmethod"alipay_wechat_pay"for one. - Paying again while a code is live answers the same code.
payment.walletsstays on the order while its wallet payment is open, however little time is left to pay; a new wallet payment can't be opened with less than 15 minutes left. - WeChat can't scan a code shown on the same phone it runs on. You can't tell which device the user is on, so offer both ways: save the image and scan it from the album (WeChat, then Scan, then Album), or open the WeChat Pay link on another screen. On a phone, the Alipay link opens the Alipay app.
- Over MCP,
pay_order's answer carries the code as an image block when it holds a live code, unless the image couldn't be fetched; then there is no image block.structuredContenthas the code and both links either way.
After payment
Read the order with GET /v1/orders/{orderId} (get_order, fortrip orders get) until it is no longer confirming. Its status:
| Status | Meaning |
|---|---|
awaiting_payment | Created and unpaid; nothing is charged. It can be paid until payment.expiresAt, by payment.url or another way the order offers. |
expired | The time to pay ran out with nothing paid, and nothing was charged. It can't be paid any more: get offers again and order anew. |
confirming | Paid, or the amount held, and the booking is with the hotel. Next comes confirmed once the hotel confirms and the payment is charged, failed if the hotel can't confirm, or needs_attention if a person has to resolve it. |
confirmed | The hotel confirmed the booking and the payment is charged. confirmationNumber is the hotel's number, null until the hotel gives one. |
failed | No stay was booked. payment.status says what became of the money: released when a hold was let go, refund_pending then refunded when a charge is refunded in full, unpaid when the payment never went through. |
cancelling | A cancellation is under way with the hotel. If the hotel refuses it, the order reads as it did before, with cancel.reason refused. |
cancelled | Cancelled under its terms. The refund is what was paid less the penalty: payment.status is refund_pending then refunded, released when the amount was only held, or stays paid when the penalty takes the whole amount. |
needs_attention | Something needs a person to resolve, and Fortrip is looking into it. The user can email info@fortrip.ai with the order's reference. |
And its payment.status, which says where the money is:
| Status | Meaning |
|---|---|
unpaid | Nothing is charged or held. |
authorized | The amount is held, not charged: a shared payment token, or a card on the payment link. It is charged when the hotel confirms, and released if the hotel can't. |
paid | Charged. Alipay and WeChat Pay, and a method on the payment link that can't be held, are charged at once, before the hotel confirms. |
released | A hold was let go; nothing was charged. |
refund_pending | A refund is under way and hasn't completed yet. |
refunded | Refunded: in full when no stay was booked, or what was paid less the penalty when the order was cancelled. |
items[0].hotel.confirmationNumberis the hotel's confirmation number, once it gives one. The confirmation is also emailed to the contact.manageUrlis the order's page on fortrip.ai, for the user signed in to the account that ordered it.
Cancel and refund
Orders can't be changed through the API. The order's cancel says whether it can be cancelled now (available, and reason when it can't) and, when it can, the penalty and the refund, what was paid less the penalty, both in the charge currency. Show the user both and wait for a yes, then cancel with cancel_order, fortrip orders cancel or POST /v1/orders/{orderId}/cancel.
The order is then cancelling while the hotel cancels, and cancelled once it has; the refund goes back to the payment the order was paid with. A booking that fails is released or refunded in full without a cancellation. For a change, or a cancellation these can't make, the user emails info@fortrip.ai with the order's reference.