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:

ScopeWhat it allows
search.runSearch availability
quote.createLock a price for fifteen minutes
booking.createBook, or hold, a locked price
booking.readRead bookings and download vouchers
booking.amendChange dates, rooms or travellers
booking.cancelCancel
client.manageCreate and read the agency's own clients
ticket.open, ticket.readSupport threads
wallet.readBalance and statement
report.readAnalytics

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: 118

Over 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:

CodeHTTPMeans
unauthenticated401Missing, wrong, revoked or expired key
forbidden403The key does not hold that scope
account_not_approved403The agency can search and quote, but not book yet
not_found404Does not exist, or is not yours — we do not distinguish
quote_expired409Take a new quote
price_changed409The price moved; details has both figures
sold_out409Gone while you were holding it
insufficient_funds402Wallet plus credit limit cannot cover it
hold_not_available422Non-refundable, on request, or inside its window
hold_expired409The hold ended; search again
rate_limited429Slow down; see Retry-After
supplier_failed502A 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

CallWhat it does
GET /v1/bookingsYour bookings, newest first
GET /v1/bookings/{id}One booking in full, with its money position and timeline
GET /v1/bookings/{id}/voucherThe voucher as a PDF
POST /v1/bookings/{id}/amendChange dates, rooms or travellers
POST /v1/bookings/{id}/cancelCancel, 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/wallet

Returns 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