
# Migrating to TTDbooking

If you already integrate the Stays API, moving to TTDbooking is a **host and credential swap**. The contract is unchanged: same paths, same methods, same JSON bodies, same field names, same enums, same error codes, same booking flow.

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.

---

## 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-register your callback URL with us, and allowlist our new egress                                          | Config       |
| 4   | If you filter inbound webhooks by `User-Agent`, update the value — see [Callback changes](#callback-changes) | Code (small) |
| 5   | _Optional but recommended:_ verify the new callback signature                                                | Code         |

Items 1–3 are configuration. Item 4 is the only change that can silently break you, and only if you allowlist on `User-Agent`. Item 5 is new capability, not a requirement.

---

## 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 until roughly five minutes before `expiresAt` and refresh then. Note that 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
}
```

Events are unchanged: `order_created`, `order_paid`, `order_cancelled`.

Three things to do:

**1. Re-register your callback URL.** We do not inherit your existing registration. Send us the URL, or set `callbackUrl` on each Book request.

**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. The only wire-level value that changes is the callback `User-Agent`.

**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.
