
# 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](#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](#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](#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:

```json
{
  "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.
- `HotelStaticDetail` remains **[DEVELOPMENT]**. Keep using `HotelsMetadata` in 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**

```bash
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**

```bash
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:

```json
{ "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**:

```json
{
  "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](https://developer.ttdbooking.com/hotel-api/docs/guides/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:

```http
User-Agent: ttdbooking-webhook/1.0
```

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

```http
X-TTD-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-TTD-Event-Id: evt_01HQ8Z3K9M2N4P6R8T0V2X4Y6A
```

- `t` — 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.

```javascript
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);
}
```

```python
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.
- [ ] `CheckAvail` price-change handling still works — exercise the recheck loop.
- [ ] `Book` returning status `1` (Confirming) or `0` (Unknown) is polled via `QueryOrders`, not rejected.
- [ ] `Cancel` returns the expected `serviceFee` and `status`.
- [ ] 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](https://developer.ttdbooking.com/hotel-api/docs/guides/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](#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](#2-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](#4-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](/hotel-api/docs/guides/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.
