
# Order Status Callback

## Overview

Rather than repeatedly querying an order to find out whether anything has changed, you can have TTDbooking push the change to you. Register a callback URL and we will send an HTTP request to it as the order moves through its lifecycle.

## 🔄 How it works

1. **Registration** — you supply your callback URL on the Book API request that creates the order.
2. **Event trigger** — a change to the order's state (payment settled, order cancelled, and so on) causes a callback to be raised.
3. **Async delivery** — callbacks are dispatched out of band, so a slow or unreachable endpoint never holds up order processing.
4. **Retry on failure** — a failed delivery is retried automatically, with the interval growing on each attempt (exponential backoff).
5. **Idempotency is your responsibility** — network faults and similar conditions mean the sequence of pushed events will not always line up with the order's normal state transitions. That cannot be fully prevented at our end, so your handler must tolerate duplicate and out-of-sequence events.

## Integration steps

### Step 1: Build your callback endpoint

Stand up an HTTPS endpoint that accepts `POST` requests carrying a JSON body.

**Requirements:**

- Reachable from the public internet
- HTTPS
- Responds with an HTTP 2xx status when the callback is accepted
- Does its real work asynchronously

**Example implementation:**

```go
func handleOrderCallback(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var payload struct {
        CustomerReferenceNo string `json:"customerReferenceNo"`
        HotelConfirmNo      string `json:"hotelConfirmNo"`
        Event               string `json:"event"`
        EventTime           int64  `json:"eventTime"`
    }

    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // Validate required fields
    if payload.CustomerReferenceNo == "" || payload.Event == "" {
        http.Error(w, "Missing required fields", http.StatusBadRequest)
        return
    }

    // Process asynchronously to avoid timeout
    go processOrderCallback(payload)

    // Return immediately
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "received"})
}
```

### Step 2: Register the callback URL on the Book request

Pass the endpoint you built as `callbackUrl` when you call the Book API:

```json
{
  "customerReferenceNo": "REF123456",
  "ratePkgId": "pkg_123456",
  "holder": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@example.com"
  },
  "guests": [
    {
      "roomIndex": 1,
      "firstName": "John",
      "lastName": "Doe"
    }
  ],
  "callbackUrl": "https://your-domain.com/webhook/order-callback"
}
```

**Points to note:**

- `callbackUrl` is optional
- It must be a well-formed HTTPS URL
- Every event raised for that order goes to this one URL
- The value is persisted against the order and reused for all later callbacks

### Step 3: Handle the callback events

These are the events your endpoint can currently receive:

| Event             | Description                                       | When Triggered                               |
| ----------------- | ------------------------------------------------- | -------------------------------------------- |
| `order_created`   | The order exists but payment is still outstanding | Once the order has been created successfully |
| `order_paid`      | Payment against the order has settled             | Once payment has been verified               |
| `order_cancelled` | The order has been cancelled                      | Once the cancellation takes effect           |

> **Note:** `order_confirmed`, `order_updated`, `order_checkedin` and `order_checkedout` are on the roadmap but are not delivered today. Write your handler so that an unrecognised event type is ignored safely rather than treated as an error.

Every callback body takes this shape:

```json
{
  "customerReferenceNo": "REF123456",
  "hotelConfirmNo": "HCN789012",
  "event": "order_paid",
  "eventTime": 1704067200000
}
```

**Payload fields:**

| Field                 | Type   | Description                                                               | Example         |
| --------------------- | ------ | ------------------------------------------------------------------------- | --------------- |
| `customerReferenceNo` | string | The reference you assigned to the order on booking                        | `REF123456`     |
| `hotelConfirmNo`      | string | Confirmation number issued by the hotel — can arrive blank on some events | `HCN789012`     |
| `event`               | string | Which lifecycle event fired                                               | `order_paid`    |
| `eventTime`           | int64  | When the event occurred, as a Unix timestamp in milliseconds              | `1704067200000` |

**Request headers:**

```http
Content-Type: application/json
User-Agent: ttdbooking-webhook/1.0
X-TTD-Event-Id: evt_01HQ8Z3K9M2N4P6R8T0V2X4Y6A
X-TTD-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

> **Migrating from the previous platform?** The `User-Agent` changed from `hotel-be-webhook/1.0` to `ttdbooking-webhook/1.0`. If your endpoint allowlists user agents, add the new value or callbacks will fail silently. See [Migrating to TTDbooking](https://developer.ttdbooking.com/hotel-api/docs/guides/migrating-to-ttdbooking).

### Step 4: Verify the signature (recommended)

Every delivery is signed once a webhook signing secret has been issued for your credential. Until then callbacks arrive unsigned and your handler keeps working unchanged — verification is additive.

- `X-TTD-Signature` carries `t=<unix seconds>` and `v1=<hex>`, where `v1` is the HMAC-SHA256 of `"{t}.{rawBody}"` keyed with your signing secret.
- `X-TTD-Event-Id` is stable across every retry of the same event, so it is the natural idempotency key.

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

Your signing secret is issued with your credentials and is **not** your `appSecret`. It is rotated independently — contact us to rotate it.

## Support

Questions about callback delivery, or an endpoint that is not receiving events? Contact us at [integrations@ttdbooking.com](mailto:integrations@ttdbooking.com).

---

Full API reference: [developer.ttdbooking.com/hotel-api/docs](https://developer.ttdbooking.com/hotel-api/docs)
