Premium Choice Trade — API guide
For a developer connecting an agency's own system to the platform. Everything here is agency-facing: you see the rates your agency buys at, and nothing about what the stay cost us or what we made on it.
- Base URL:
https://<your-api-host> - Machine-readable spec:
GET /openapi.json— always current, generated from the code - Interactive browser:
GET /docs
1. Getting a key
An owner at the agency creates keys in the portal under Settings → API keys.
A key looks like pct_live_3f9a2c11_kCq…. The secret half is shown once, at the moment it is created, and never again — we store only a hash of it, so if it is lost the only remedy is to revoke it and make another.
Send it as a bearer token:
GET /v1/bookings HTTP/1.1
Authorization: Bearer pct_live_3f9a2c11_kCq…What a key can and cannot do
A key is deliberately weaker than the person who created it. When you create one you pick from:
| Scope | What it allows |
|---|---|
search.run | Search availability |
quote.create | Lock a price for fifteen minutes |
booking.create | Book, or hold, a locked price |
booking.read | Read bookings and download vouchers |
booking.amend | Change dates, rooms or travellers |
booking.cancel | Cancel |
client.manage | Create and read the agency's own clients |
ticket.open, ticket.read | Support threads |
wallet.read | Balance and statement |
report.read | Analytics |
No key can ever top up a wallet, change a markup rule, manage people, or create another key. Those stay with a signed-in person, on purpose: a credential sitting in a CI server should not be able to move money that is not already committed.
Rate limits
Every key has a per-minute limit (120 by default). Every response carries:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118Over the limit you get 429 with a Retry-After header in seconds. Ask us if you need a higher limit — it is a per-key setting, not a plan.
2. Errors
Every error has the same shape, and every one tells you what to do about it:
{
"error": {
"code": "insufficient_funds",
"message": "Your wallet cannot cover this booking.",
"next": "Top up your wallet or pay by card.",
"details": { "shortfall": { "amount": 45000, "currency": "AED" } }
}
}Read code in your software and show message and next to a person. The ones you will meet:
| Code | HTTP | Means |
|---|---|---|
unauthenticated | 401 | Missing, wrong, revoked or expired key |
forbidden | 403 | The key does not hold that scope |
account_not_approved | 403 | The agency can search and quote, but not book yet |
not_found | 404 | Does not exist, or is not yours — we do not distinguish |
quote_expired | 409 | Take a new quote |
price_changed | 409 | The price moved; details has both figures |
sold_out | 409 | Gone while you were holding it |
insufficient_funds | 402 | Wallet plus credit limit cannot cover it |
hold_not_available | 422 | Non-refundable, on request, or inside its window |
hold_expired | 409 | The hold ended; search again |
rate_limited | 429 | Slow down; see Retry-After |
supplier_failed | 502 | A bed bank did not answer. Retry is safe |
Money is always integer minor units with a currency: { "amount": 254100, "currency": "AED" } is AED 2,541.00. OMR, KWD and BHD have three decimals, not two.
3. The booking flow
Three calls, in order. You cannot skip a step: a price is only bookable through a quote.
Search
POST /v1/search
Content-Type: application/json
{
"destination": { "cityCode": "DXB" },
"checkIn": "2027-02-03",
"checkOut": "2027-02-05",
"rooms": [{ "adults": 2, "childAges": [] }]
}Answers quickly with the first page and "pending": true when suppliers are still replying. Poll GET /v1/search/{sessionId} until pending is false — usually one or two seconds.
Each result carries an explanation: where the rate came from, which contract or rate plan, the taxes and the FX used. Show it. It is the difference between a price and a number.
Quote
POST /v1/quotes
{ "offerId": "…from the search…" }This re-prices the offer against live inventory, holds it, and returns a quoteId with an expiry. If the price moved you get 409 price_changed with both figures — decide, then quote again.
Book
POST /v1/bookings
{
"quoteId": "…",
"travellers": [{ "firstName": "Karim", "lastName": "Haddad", "type": "adult" }],
"payment": "wallet",
"agreed": true
}Retries are safe. The quote is claimed under a lock, so sending the same quoteId twice — a timeout, a double click, a retry in your queue — returns the booking that already exists rather than making a second one. You do not need your own idempotency key.
Hold instead of pay
A refundable rate can be held: booked and reserved now, nothing charged, until the quote's
Send "hold": true and no payment:
POST /v1/bookings
{ "quoteId": "…", "travellers": [ … ], "hold": true, "agreed": true }The booking comes back held with heldUntil. Confirm and pay before then, or it releases itself at no charge and the room goes back on sale:
POST /v1/bookings/{id}/confirm
{ "payment": "wallet", "agreed": true }A quote whose holdableUntil is null cannot be held (422 hold_not_available): the rate is non-refundable, on request, or already inside its cancellation window. Cancelling a held booking is a release and charges nothing.
After booking
| Call | What it does |
|---|---|
GET /v1/bookings | Your bookings, newest first |
GET /v1/bookings/{id} | One booking in full, with its money position and timeline |
GET /v1/bookings/{id}/voucher | The voucher as a PDF |
POST /v1/bookings/{id}/amend | Change dates, rooms or travellers |
POST /v1/bookings/{id}/cancel | Cancel, with the charge stated in the response |
An amendment answers with the difference and "applied": false. Nothing changes until you send it again with "confirm": true — and then the difference settles against your wallet.
4. Paying
Bookings draw on your wallet. Check before you commit:
GET /v1/walletReturns your balance, your credit limit, and what is available to spend. A booking larger than that is refused with 402 insufficient_funds and nothing is booked.
Top-ups are deliberately not available to an API key. A person tops up in the portal.
5. Your client's price
Everything above is your net rate — what you pay us. What you charge your own client is yours, and we never take it.
If you want your client to pay by card directly, book with "payment": "client_link" and a
the net rate settles your booking and the difference lands in your wallet as credit you can spend on the next one.
6. What we have not built yet
Said plainly so you do not go looking:
- No webhooks for agencies. You poll. Booking status changes are rare enough that a poll on your own schedule is honest; if you need push, tell us and it will be built.
- No sandbox. There is one environment. Use a wallet with a small balance while testing, and cancel what you create.
- No bulk or batch endpoints. One search, one booking.
- No flights, transfers or activities. Hotels only, for now.
7. MCP, for AI assistants
If you want an assistant — Claude, or anything else that speaks the Model Context Protocol — to search and book through us, point it at:
POST /v1/mcp
Authorization: Bearer pct_live_…It is a standard MCP server over HTTP. The tools it offers are decided by the key's scopes: a key that cannot book is not shown a booking tool at all, so the assistant plans around it rather than trying and failing.
Two behaviours worth knowing:
- `create_booking` and `cancel_booking` require `agreed: true`. An assistant asked to "find me a hotel" cannot end up spending money.
- Prices only come from the platform. The instructions tell the model never to state a price that did not come from a search or a quote, so it cannot invent one.
Questions
Open a support thread from the portal, or POST /v1/tickets with a key that holds
