2. Guides
Migrating to TTDbooking
If you already integrate the Stays API, moving to TTDbooking is a host, credential and catalogue swap. The contract is unchanged: same paths, same methods, same JSON bodies, same field names, same enums, same error codes, same booking flow. What does change is the catalogue behind it — hotel and destination ids are ours, and the ones you have stored will not resolve here.
Most partners finish in under an hour.
Your request and response bodies do not change. Not one field is renamed, added or removed. If your integration is generated from our schemas, regenerating is optional — your existing models still deserialize correctly.
The identifiers carried inside them do change. Hotel and destination ids belong to our catalogue, not your previous platform's. This is the one step that fails silently — a search that reuses stored ids is a valid request, so it is answered normally, with an empty list. See Identifiers do not carry over.
TL;DR — what you change
| # | Change | Effort |
|---|---|---|
| 1 | Point your base URL at https://api.ttdbooking.com (production) or https://api-test.ttdbooking.com (test) |
Config |
| 2 | Swap in your new appKey / appSecret. Your old credentials will not work against TTDbooking |
Config |
| 3 | Re-pull hotel and destination ids. Stored ids from your previous platform resolve to nothing here — see Identifiers do not carry over | Data |
| 4 | Send callbackUrl on each Book request (there is no standing registration to inherit), and allowlist our new egress |
Config |
| 5 | If you filter inbound webhooks by User-Agent, update the value — see Callback changes |
Code (small) |
| 6 | Optional but recommended: verify the new callback signature | Code |
Items 1, 2 and 4 are configuration. Items 3 and 5 are the two that can break you silently — item 3 returns successful, empty searches, and item 5 only bites if you allowlist on User-Agent. Item 6 is new capability, not a requirement.
Identifiers do not carry over
The contract is identical; the values inside it are ours. Every hotel and destination id in TTDbooking belongs to TTDbooking's catalogue, and an id issued to you by your previous platform names nothing here.
This is the one migration step that fails silently. Reusing a stored shortlist is a perfectly valid request, so it is answered normally — successfully, and with nothing in it:
{
"code": 0,
"msg": "success",
"data": { "list": [], "total": 0, "basic": { "sessionId": "…" } }
}There is no error to catch, because nothing failed: you asked about properties this account does not sell under those codes. (Responses now also carry a basic.statusRemark saying so when none of the codes you sent matched.)
Where each id comes from
| You send | Where the value comes from |
|---|---|
hotelIds on HotelList |
The hotelId values in a previous HotelList or HotelsMetadata response from this account |
destinationId |
The id of a node returned by Destinations — a UUID, not a numeric code |
destinationName |
Free text, e.g. "Dubai". The quickest way to run a first search before you have re-pulled anything |
Re-pull HotelsMetadata once for each destination you trade and re-key your catalogue against the hotelId values it returns. Until that is done, search by destinationName or destinationId rather than by hotelIds.
If re-keying your catalogue is not practical, we can load a code book that maps your existing hotel codes onto our properties. The API then speaks your codes in both directions — you send yours, and you get yours back in every response. Send us a CSV of your hotel codes with the property name, address, city and country for each (plus a GIATA id where you hold one) and we will map and load them. Ask your account manager to start this.
Endpoint mapping
Only the host changes. Every path, method and body is identical.
| Endpoint | Old | New | Body unchanged? |
|---|---|---|---|
| Host (prod) | https://api.hotelbyte.com |
https://api.ttdbooking.com |
— |
| Host (test) | https://api-test.hotelbyte.com |
https://api-test.ttdbooking.com |
— |
| Ticket | POST /api/auth/ticket |
POST /api/auth/ticket |
✅ Yes |
| Destinations | POST /api/search/destinations |
POST /api/search/destinations |
✅ Yes |
| HotelsMetadata | POST /api/search/hotelsMetadata |
POST /api/search/hotelsMetadata |
✅ Yes |
| HotelStaticDetail | POST /api/search/hotelStaticDetail |
POST /api/search/hotelStaticDetail |
✅ Yes |
| HotelList | POST /api/search/hotelList |
POST /api/search/hotelList |
✅ Yes |
| HotelRates | POST /api/search/hotelRates |
POST /api/search/hotelRates |
✅ Yes |
| CheckAvail | POST /api/search/checkAvail |
POST /api/search/checkAvail |
✅ Yes |
| Book | POST /api/trade/book |
POST /api/trade/book |
✅ Yes |
| QueryOrders | POST /api/trade/queryOrders |
POST /api/trade/queryOrders |
✅ Yes |
| Cancel | POST /api/trade/cancel |
POST /api/trade/cancel |
✅ Yes |
Unchanged along with the paths:
- The
{ "code": 0, "msg": "success", "data": { … } }envelope on every response. - Every business error code, including
100001111(ARI changed).100001112(credit limit) remains reserved and not currently emitted — no code path returns it, so there is nothing to change on your side. - Request headers:
Authorization,Content-Type,Request-Id,Trace-Id,Session-Id,Language,Currency,Timeout-Milliseconds. - Response headers:
Request-Id,Trace-Id,Server-Cost-Milliseconds,Session-Id. - The booking funnel: HotelList → HotelRates → CheckAvail → Book → QueryOrders / Cancel.
HotelStaticDetailremains [DEVELOPMENT]. Keep usingHotelsMetadatain production.
Renamed values
No field, header or enum was renamed. Three values differ, and only one of them is on the wire:
| What | Old | New | Affects you if… |
|---|---|---|---|
Webhook User-Agent |
hotel-be-webhook/1.0 |
ttdbooking-webhook/1.0 |
…you allowlist or filter callbacks by User-Agent. Check this. |
| Demo credential | hotelbyte_api_demo |
ttdbooking_api_demo |
…you use the sandbox credential in test |
| Sample env var names | HOTELBYTE_* |
TTDBOOKING_* |
…never. These are names in our sample code, not part of the contract — call yours whatever you like |
Authentication
Same request, same response shape, new host and new secrets.
Before
curl -X POST https://api.hotelbyte.com/api/auth/ticket \
-H "Content-Type: application/json" \
-d '{"appKey":"<your-old-key>","appSecret":"<your-old-secret>","ttl":3600}'After
curl -X POST https://api.ttdbooking.com/api/auth/ticket \
-H "Content-Type: application/json" \
-d '{"appKey":"<your-ttd-key>","appSecret":"<your-ttd-secret>","ttl":3600}'Both return:
{ "code": 0, "msg": "success", "data": { "ticket": "<jwt>" } }Then send Authorization: Bearer <ticket> on every other call, exactly as before.
Keep your ticket caching. Tickets are meant to be reused — cache each one and refresh it roughly five minutes before its lifetime elapses (the ttl you requested at issuance; 24 hours by default). Expiry is absolute and fixed at issuance: it is not extended by API activity, so a busy integration still expires on schedule and must renew proactively. Requesting a fresh ticket per call is the single most common cause of hitting the rate limit during a migration.
Callback changes
Order status callbacks work the same way: you supply callbackUrl on the Book request, we POST order status changes to it, you return 2xx.
The payload is byte-for-byte identical:
{
"customerReferenceNo": "REF123456",
"hotelConfirmNo": "HCN789012",
"event": "order_paid",
"eventTime": 1704067200000
}The three events you already handle are unchanged: order_created, order_paid,
order_cancelled. There is a fourth, order_modified, which reports an
amendment to a confirmed order — including a hotel confirmation number issued
after booking. It is outbound-only and can arrive more than once for one order
(once per distinct amendment), so de-duplicate it on X-TTD-Event-Id. See the
Order status callback.
Three things to do:
1. Register your callback URL on each booking. There is no standing per-credential registration to inherit: the callback URL travels on the callbackUrl field of each Book request, and every event for that order goes to the URL it was booked with.
2. Update any User-Agent allowlist. Our callbacks now identify as:
User-Agent: ttdbooking-webhook/1.0If your endpoint rejects unknown user agents, callbacks will silently fail until you add this. This is the one change that breaks quietly — please check it explicitly.
3. Verify the signature (recommended). This is new capability, not a breaking change. The previous integration sent callbacks unsigned. Your existing handler keeps working unchanged; verification is additive.
Status: callback signing is active in both the test and production environments. Deliveries are signed as soon as a webhook signing secret has been issued for your credential — ask us for one and you can exercise verification in test straight away. Until a secret is issued, your callbacks are delivered unsigned exactly as before, so nothing changes for you until you are ready.
Each signed callback carries:
X-TTD-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-TTD-Event-Id: evt_01HQ8Z3K9M2N4P6R8T0V2X4Y6At— Unix seconds when we signed the request.v1— HMAC-SHA256 of"{t}.{rawBody}", keyed with your webhook signing secret, hex encoded.X-TTD-Event-Id— stable per delivery attempt of an event; use it for idempotency.
Verify against the raw request body, before any JSON parsing or re-serialization — re-encoding changes the bytes and the signature will not match.
const crypto = require("crypto");
function verifyTtdSignature(rawBody, headerValue, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
headerValue.split(",").map((kv) => kv.split("=")),
);
const timestamp = Number(parts.t);
if (!timestamp || !parts.v1) return false;
// Reject replays outside the tolerance window.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time compare — a plain === leaks timing information.
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hashlib, hmac, time
def verify_ttd_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
try:
timestamp = int(parts["t"])
received = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, received)Your signing secret is issued with your credentials and is not your appSecret. Rotate it independently.
Retries and idempotency are unchanged. Failed deliveries retry with exponential backoff, and duplicate deliveries remain possible — keep your handler idempotent, keyed on customerReferenceNo + event, or on X-TTD-Event-Id.
Cutover plan
1. Validate in test
Point a non-production build at https://api-test.ttdbooking.com with your TTD test credentials and confirm:
- Ticket issues successfully, and your client caches and reuses it.
- A full funnel completes: HotelList → HotelRates → CheckAvail → Book.
-
CheckAvailprice-change handling still works — exercise the recheck loop. -
Bookreturning status1(Confirming) or0(Unknown) is polled viaQueryOrders, not rejected. -
Cancelreturns the expectedserviceFeeandstatus. - Error paths still map correctly, especially
100001111(ARI changed). (100001112, credit limit, is reserved and not currently emitted — there is no code path to exercise.) - Callbacks reach your endpoint — with the new
User-Agent. - If you implemented it: signature verification passes, and a deliberately corrupted signature fails.
2. Re-certification
If you were already certified, you generally do not need a full re-run. We ask for a short confirmation pass covering the funnel and one cancellation. If your integration changed beyond the host and credentials, we will schedule the full certification cases.
3. Switch production
Deploy the new host and credentials together — they are a matched pair, and a new host with old credentials fails closed on 100000401.
Nothing about the switch is stateful on your side: there is no migration window, no data copy, and no in-flight state held in your client.
4. Rollback
Until the legacy host is switched off, rollback is reverting your configuration — put back the previous base URL and credentials and redeploy. There is no schema change to undo.
In-flight bookings: orders placed against the legacy host remain queryable and cancellable there for the whole dual-run window. Because order identifiers are yours (customerReferenceNo), an order created on one host is not automatically visible on the other. Let orders placed before your switch settle on the legacy host, or contact us to have them migrated. Plan your switch for a low-volume window and you will have very few of these.
Timeline
Both hosts run in parallel throughout the migration. You choose when to switch.
The legacy hosts will stop serving TTD partner traffic no sooner than 90 days after the switchover date is announced. The exact date is still to be confirmed — you will receive it in writing, and nothing is switched off before then.
You will get written notice with the confirmed date, and a reminder before it takes effect. Until then nothing is switched off.
FAQ
Do my request or response bodies change?
No. No field is renamed, added or removed. Enums, error codes and the { code, msg, data } envelope are identical.
Do the values inside them change?
Yes — two of them. Hotel and destination ids are ours and must be re-pulled (Identifiers do not carry over), and the callback User-Agent changes. Everything else on the wire is the same.
Do I need to re-certify? Usually a short confirmation pass, not a full re-run. See Re-certification.
Will my in-flight bookings survive the switch? Orders live with the host they were created on. Both hosts stay live during the dual-run window, so nothing is lost — but query and cancel an order on the host that created it. See Rollback.
How do I get new credentials?
Email integrations@ttdbooking.com. You will receive a test appKey/appSecret first, then production credentials plus your webhook signing secret once your test pass is confirmed.
Can I run both integrations at once? Yes. Both hosts are live during the dual-run window. Run TTDbooking in test while production stays on the legacy host for as long as you need.
Do I have to implement signature verification? No. It is additive — if we have not issued you a signing secret, your callbacks arrive unsigned and your existing handler keeps working. Signing is active in test as well as production, so once you ask us for a secret you can verify end to end in test before you go live. We recommend doing exactly that.
My old credentials return 100000401 against the new host. Is that a bug?
No, that is expected. Credentials are not shared between the two platforms. Use the ones issued to you for TTDbooking.
Are rate limits the same?
Comparable in shape, but they are TTDbooking's own and no ceiling from your previous account carries over. Ceilings are per second, per endpoint, per client IP: 100/second on search and content, 50/second on QueryOrders, 10/second on the transactional trio (CheckAvail, Book, Cancel) and 5/second on ticket issuance. Responses now include X-RateLimit-Limit, -Remaining and -Reset, so you can measure your real headroom during the migration instead of estimating it. Send us your expected volumes and your egress IP addresses before cutover. See Rate limit.
Need help?
Email integrations@ttdbooking.com with your appKey (never your secret) and, where relevant, the Request-Id and Trace-Id of the call you are asking about. Those two headers let us find your exact request in seconds.

