Travel To Discover

Loading documentation…

2. Guides

Export .md

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: its host name must resolve to public addresses only. We resolve it before every delivery and never connect to a private, loopback, link-local or shared (100.64.0.0/10) address
  • HTTPS
  • Responds with an HTTP 2xx status when the callback is accepted, at the URL itself. Redirects are not followed: a 3xx counts as a failed delivery and is retried like any other non-2xx
  • 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. A private or loopback address, or a host name that only exists on a private network (localhost, a single-label name, *.internal, *.local), is refused as 100000400 / ERR.PARAM
  • 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
order_modified A confirmed order has been amended Once per distinct amendment β€” including a hotel confirmation number issued after booking

order_modified is outbound-only, and it can repeat. Unlike the other three it may legitimately arrive more than once for one order, once per distinct amendment, so de-duplicate it on the X-TTD-Event-Id header rather than on customerReferenceNo + event. It is also the one event you must not send us: POSTing order_modified to /api/callback/order is refused with a param error, because the inbound receiver accepts only the three events above.

Note: order_confirmed, order_checkedin and order_checkedout are on the roadmap but are not delivered today. The event previously listed here as order_updated ships under the name order_modified and is delivered β€” see the row above. 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.

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.


Full API reference: developer.ttdbooking.com/hotel-api/docs