# Booking Window — BW Out API (`/bwOutApi/*`)

**Version 1.0 · 2026-09-22**

The server-to-server API for **BW agents**: the same catalogue, the same prices
and the same booking flow as the Agent Out API (`/outApi/*`), charged to the BW
wallet you already use in the panel.

If you are integrating `/outApi/*` instead, read
`OUT_API_AGENT_DOCUMENTATION.md`. **The two APIs are not interchangeable**: a key
issued for one is rejected by the other, and bookings made on one are invisible
to the other.

---

## 1. Basics

**Base URL**

```
https://<your-host>/bwOutApi
```

All endpoints are `POST` and take a JSON body. `Content-Type: application/json`.

Every response carries an **`X-Request-Id`** header. Keep it: it is the single
key support uses to find your call.

### 1.1 Authentication

Every request sends your key in a header. There is **no login, no OTP and no
Bearer token**.

```
x-api-key: BWX_XXXXXXXXXXXXXXXX
```

The key is your BW account's reference id (`user_reference_id`) — the same
account the BW panel signs you into. Your account must be **active and
verified**; a key on an inactive or unverified account is refused with `403`
even if the panel still works for you.

The key is the identity, so it decides three things at once: whose markup prices
the catalogue, whose wallet is charged, and whose bookings you can see. Treat it
as a password — never in a browser, a mobile app or a public repository. If it
leaks, ask us to reissue it; your existing bookings are unaffected.

| | value |
|---|---|
| Header | `x-api-key` |
| Missing | `401` `Missing 'x-api-key' header.` |
| Wrong / unknown | `401` `Invalid API key` |
| Account not active | `403` `API User account is inactive` |
| Account not verified | `403` `API User account is not verified` |

### 1.2 What differs from `/outApi/*`

Everything below is the whole of the difference. The catalogue, the pricing, the
inventory rules, the reservation flow, the cancellation policy and the GlobalTix
handling are the same code.

| | `/outApi/*` | `/bwOutApi/*` |
|---|---|---|
| Key resolves to | an Out API partner account | **your BW agent account** |
| Wallet | the Out API wallet | **your BW wallet**, through the same ledger the panel shows |
| `cmd` in responses | `x_…` | **`bw_…`** |
| Booking channel | `ACT` | **`BWAPI`** |
| Extra failure | — | **`wallet_unavailable` (503)** on booking |
| Extra field | — | **`refund_pending`** on cancellation |

Two consequences worth designing for:

1. **Your API bookings are separate from your panel bookings.** `bookings_list`
   here returns only what you booked through this API. Bookings you make in the
   BW panel are not returned, and cannot be cancelled through this API.
2. **The wallet is charged after the booking is written**, because it is settled
   by our accounts service rather than by the same database transaction. We
   handle the failure for you — see §1.5 — but it is why two answers exist here
   that do not exist on `/outApi`.

### 1.3 Pricing

Prices are **net rates plus your markup**, in your account's currency, and are
already final: there is nothing to add on your side.

Which markup applies is decided by the markup group attached to your account.
With no group attached you get the default group's rules, which is the normal
state — ask commercial if you need your own.

### 1.4 What you can sell

Two sources are available to every BW agent:

| `source` | what it is |
|---|---|
| `ACT` | our own contracted inventory |
| `GT` | GlobalTix products |

Send `source` on a request to restrict it to one; omit it to get both where the
endpoint supports it. Individual activities can still be withdrawn from your
account by commercial, in which case they are simply absent from the catalogue
and a booking attempt answers `403 activity_not_permitted`.

### 1.5 The wallet, and the two answers it adds

Your balance is the BW wallet. A booking debits it; a cancellation refunds it,
less any cancellation charge.

**On booking — `503 wallet_unavailable`.** The wallet could not be charged. We
reverse everything before answering, so:

```json
{
  "status": false,
  "replyCode": "wallet_unavailable",
  "replyMsg": "Your wallet could not be charged. The booking was not made and you have not been charged.",
  "cmd": "bw_create_booking",
  "data": { "booking_ref_no": "ACTAB12CD", "charged": false, "booking_voided": true }
}
```

`charged: false` is a statement of fact, not a guess. **Retrying is safe.** When
you were confirming a reservation, the reservation goes back to `RESERVED` with
a fresh hold window (`reservation_status: "RESERVED"` in `data`) — your stock is
still yours, so retry the same `create_booking` call rather than reserving
again.

The only case needing a human is `charged: false` together with a message asking
you to contact support: that means the reversal itself did not complete, and you
should quote the `X-Request-Id` rather than retry.

**On cancellation — `refund_pending: true`.** The booking **is** cancelled and
the refund is owed but has not yet landed in your balance. No action is needed;
it settles on our side and appears in `transactions_list`. Do not re-cancel.

### 1.6 Response envelope

Success:

```json
{
  "status": true,
  "replyCode": "success",
  "replyMsg": "…",
  "cmd": "bw_create_booking",
  "data": { },
  "api_user_ref": "BWX_XXXXXXXXXXXXXXXX"
}
```

Failure:

```json
{
  "status": false,
  "replyCode": "insufficient_balance",
  "replyMsg": "Insufficient wallet balance. Please recharge your wallet.",
  "cmd": "bw_create_booking"
}
```

Branch on `replyCode`, never on `replyMsg` — messages are wording and may be
improved; codes are contract. HTTP status and `status` always agree.

---

## 2. Endpoint summary

| # | Endpoint | Purpose |
|---|---|---|
| 1 | `POST /bwOutApi/country_list` | Countries with sellable activities |
| 2 | `POST /bwOutApi/city_list` | Cities in a country |
| 3 | `POST /bwOutApi/activity_list` | Search the catalogue (paginated) |
| 4 | `POST /bwOutApi/get_activity_by_id` | One activity in full |
| 5 | `POST /bwOutApi/get_time_slots` | Slots and prices for a date |
| 6 | `POST /bwOutApi/get_transfer_by_ids` | Transport packages |
| 7 | `POST /bwOutApi/reserve_booking` | Hold stock, charge nothing |
| 8 | `POST /bwOutApi/create_booking` | Confirm a hold, or book in one step |
| 9 | `POST /bwOutApi/release_booking` | Give a hold back |
| 10 | `POST /bwOutApi/bookings_list` | Your bookings |
| 11 | `POST /bwOutApi/booking_details` | One booking, with voucher |
| 12 | `POST /bwOutApi/cancellation_charges` | What cancelling would cost |
| 13 | `POST /bwOutApi/cancel_booking` | Cancel and refund |
| 14 | `POST /bwOutApi/transactions_list` | Your wallet ledger |

The request and response bodies of 1–6 and 10–14 are identical to the Agent Out
API's, apart from the `cmd` prefix. The Postman collection
(`Activity_BW_Out_API.postman_collection.json`) carries a working example of
every one.

---

## 3. The booking flow

### 3.1 Two steps, and why you want them

```
reserve_booking          create_booking             (or)  release_booking
  stock held               wallet charged                   stock returned
  nothing charged          booking confirmed                nothing charged
  hold_expires_at          voucher emailed
```

Reserve while your customer is still deciding or paying; confirm when their
money is yours. The hold is what stops the last seat selling to someone else
while you collect payment.

**GlobalTix (`source: "GT"`) must be reserved first.** A one-step
`create_booking` for a GT product is refused with `reserve_required`.

### 3.2 `POST /bwOutApi/reserve_booking`

Same body as a one-step `create_booking` (§3.3). Holds the stock and **re-prices
the activity**, so the reply tells you what the confirm will actually cost:

```json
{
  "status": true,
  "replyCode": "success",
  "replyMsg": "Activity reserved successfully",
  "cmd": "bw_reserve_booking",
  "data": {
    "booking_id": 9001,
    "booking_ref_no": "ACTAB12CD",
    "reservation_status": "RESERVED",
    "hold_expires_at": "2026-09-22 12:45:00",
    "hold_minutes": 15,
    "reference_id": 4,
    "source": "ACT",
    "inventory_source": "time_slot",
    "category_id": 8,
    "time_slot_id": 133267,
    "supplier_reference": null,
    "from_date": "2026-10-10",
    "to_date": "2026-10-10",
    "units": 1,
    "requested_total_price": 1349.95,
    "total_price": 1349.95,
    "price_changed": false,
    "price_check": "quoted",
    "currency": "INR",
    "wallet_balance": 48650.05
  }
}
```

- `total_price` is the **held price** and is what the confirm charges.
- `price_changed: true` means our price moved since you last read the
  catalogue — show the new figure before charging your customer.
- `price_check: "unavailable"` means we could not re-price it and held **your**
  figure; `price_changed` is then `null`, never `false`.
- Your balance is checked here but **nothing is debited**.

After `hold_expires_at` the units go back automatically and the confirm answers
`reservation_expired`.

### 3.3 `POST /bwOutApi/create_booking`

**Confirming a hold** — send only the reference:

```json
{ "booking_ref_no": "ACTAB12CD", "total_price": 1349.95 }
```

`total_price` is optional; if sent it must equal the held price or the call is
refused with `price_mismatch` (409) carrying the price we hold.

**Booking in one step** (own inventory only):

| Param | Type | Req. | Description |
|---|---|---|---|
| `reference_type` | int | **Yes** | Always `1` (activity) |
| `reference_id` | int | **Yes** | Activity id |
| `from_date` | date | **Yes** | Travel date (`YYYY-MM-DD`) |
| `to_date` | date | No | End date |
| `no_of_travellers` | object | **Yes** | `{ adult, child, infant, travellers }`; at least one adult or child |
| `total_price` | number | **Yes** | Selling price, > 0 — the amount debited |
| `currency` | string | No | Currency of `total_price` |
| `reference_object` | object | **Yes** | The activity snapshot you are booking |
| `time_slot` | int | No | Explicit time-slot id |
| `category_id` | int | No | Explicit category id |
| `cancellation_policy_id` | int | No | Chosen cancellation policy |
| `guest_info` | object | Recommended | `{ first_name, last_name, email, contact_no }` |
| `title`, `city`, `country`, `time_from`, `time_to`, `transfer_mode`, `pickup_location`, `drop_location`, `price`, `markup`, `discount` | — | No | Stored with the booking |

Units booked = `no_of_travellers.travellers`, else `adult + child`. Infants do
not consume a unit.

```json
{
  "reference_type": 1,
  "reference_id": 4,
  "from_date": "2026-10-10",
  "to_date": "2026-10-10",
  "currency": "INR",
  "time_slot": 133267,
  "cancellation_policy_id": 43,
  "no_of_travellers": { "adult": 1, "child": 0, "infant": 0, "travellers": 1 },
  "guest_info": {
    "first_name": "John", "last_name": "Doe",
    "email": "john.doe@example.com", "contact_no": "9999999999"
  },
  "total_price": 1349.95,
  "reference_object": {
    "id": "4",
    "source": "ACT",
    "activity_date": "2026-10-10",
    "inventory_source": "time_slot",
    "selected_category_id": 8,
    "isSelectedTimeSlotObject": { "id": 133267, "inventory_date": "2026-10-10" },
    "no_of_travellers": { "adult": 1, "child": 0, "infant": 0, "travellers": 1 }
  }
}
```

Success (confirm):

```json
{
  "status": true,
  "replyCode": "success",
  "replyMsg": "Activity booking created successfully",
  "cmd": "bw_create_booking",
  "data": {
    "booking_id": 9001,
    "booking_ref_no": "ACTAB12CD",
    "reservation_status": "CONFIRMED",
    "reference_id": 4,
    "source": "ACT",
    "inventory_source": "time_slot",
    "category_id": 8,
    "time_slot_id": 133267,
    "supplier_reference": null,
    "voucher_pdf": null,
    "from_date": "2026-10-10",
    "to_date": "2026-10-10",
    "units": 1,
    "total_price": 1349.95,
    "currency": "INR",
    "wallet_balance": 47300.10
  },
  "api_user_ref": "BWX_XXXXXXXXXXXXXXXX"
}
```

`wallet_balance` is read back from the ledger after the charge. It can be `null`
if that read fails — the charge still succeeded; call `transactions_list` for
the authoritative figure.

For a GlobalTix booking, `supplier_reference` is GlobalTix's own reference and
`voucher_pdf` is the e-ticket **when it is ready**. GlobalTix issues tickets
asynchronously, so `null` means "not yet": read `booking_details` again, or wait
for the follow-up email we send the moment it lands.

**Failures:** `insufficient_balance`, `wallet_unavailable` (§1.5),
`duplicate_booking` (same activity and amount within a minute),
`activity_not_permitted`, `reserve_required` (GT one-step),
`price_mismatch`, `already_confirmed`, `reservation_expired`,
`reservation_released`, `booking_cancelled`, `not_found`, `bad_request`.

**Do not retry blindly.** If a call times out with no answer, check
`bookings_list` before trying again. A 503 `wallet_unavailable` is the one
failure that is always safe to retry, because it tells you nothing was charged.

### 3.4 `POST /bwOutApi/release_booking`

```json
{ "booking_ref_no": "ACTAB12CD" }
```

Gives a hold back immediately. No money has moved, so there is nothing to
refund. Releasing a hold that is already gone is a **200**, not an error — a
retry after a timeout must not look like a failure. A `CONFIRMED` booking is not
releasable and answers `already_confirmed`; use `cancel_booking`.

---

## 4. Cancellation

### 4.1 `POST /bwOutApi/cancellation_charges`

```json
{ "booking_ref_no": "ACTAB12CD" }
```

Quotes what cancelling **right now** would cost, without changing anything. The
charge is a percentage of the booking, decided by how many **hours before
travel** you are cancelling. Cancelling earlier than the whole policy window is
fully refundable.

For a GlobalTix booking the terms are GlobalTix's own, frozen at reserve time —
so a policy change at the supplier cannot re-price a booking you already paid
for.

### 4.2 `POST /bwOutApi/cancel_booking`

```json
{ "booking_ref_no": "ACTAB12CD", "remarks": "Guest cancelled" }
```

```json
{
  "status": true,
  "replyCode": "success",
  "replyMsg": "Booking cancelled successfully",
  "cmd": "bw_cancel_booking",
  "data": {
    "booking_ref_no": "ACTAB12CD",
    "booking_status": "CANCELLED",
    "currency": "INR",
    "total_price": 1349.95,
    "cancellation_amount": 337.49,
    "refund_amount": 1012.46,
    "charge_reason": "25% within 48 hours of travel",
    "hours_before_travel": 36,
    "wallet_balance": 48312.56,
    "refund_pending": false,
    "inventory_restored": true,
    "cancelled_by": { }
  }
}
```

`refund_pending: true` means the cancellation stands and the refund is still
settling (§1.5) — no action, and do not re-cancel.

A **hold** is not cancellable: it was never charged. Release it instead.
Cancelling twice answers `already_cancelled`, never a second refund.

For GlobalTix bookings we cancel at the supplier **before** touching money, so
the refund can never pay you back for tickets that are still live. If the
product is non-cancellable the call is refused with `gt_not_cancellable` before
anything changes.

---

## 5. Emails

Sent from your registered agent address, to you and — when you supply
`guest_info.email` — to your guest as well:

| When | What |
|---|---|
| A booking is confirmed | Confirmation with the booking summary, and the supplier e-ticket when it is ready |
| A GlobalTix e-ticket arrives later | A short follow-up carrying the ticket |
| A booking is cancelled | The charge, the refund and your balance |

A guest address that is not a plausible address is dropped rather than risking
the whole message — you still get yours.

---

## 6. Errors

| `replyCode` | HTTP | Meaning |
|---|---|---|
| `unauthorized` | 401 | Missing, unknown or wrong-channel key |
| `unauthorized` | 403 | Account inactive or not verified (the message says which) |
| `bad_request` | 400 | Validation failed |
| `not_found` | 404 | No such booking **of yours** |
| `duplicate_booking` | 400 | Same activity and amount within a minute |
| `insufficient_balance` | 400 | Not enough in the wallet |
| `wallet_unavailable` | 503 | Wallet could not be charged; nothing was charged |
| `activity_not_permitted` | 403 | This activity is not available on your account |
| `source_not_granted` | 403 | That source is not enabled for you |
| `source_not_supported` | 400 | That source cannot serve this endpoint |
| `reserve_required` | 400 | GlobalTix must be reserved before confirming |
| `price_mismatch` | 409 | Sent total differs from the held price |
| `reservation_expired` | 409 | The hold lapsed; reserve again |
| `reservation_released` | 409 | The hold was released |
| `already_confirmed` | 400/409 | Already confirmed |
| `already_cancelled` | 400 | Already cancelled |
| `booking_cancelled` | 400 | The booking is cancelled |
| `gt_not_cancellable` | 409 | GlobalTix product cannot be cancelled |
| `gt_cancel_refused` | 409 | GlobalTix refused the cancellation |
| `gt_unavailable` | 503 | GlobalTix is not responding |
| `gt_cancelled_not_settled` | 500 | Cancelled at the supplier, refund unfinished — **do not retry**, contact support |
| `error` | 500 | Unexpected — quote the `X-Request-Id` |

---

## 7. cURL

```bash
curl -X POST https://<your-host>/bwOutApi/activity_list \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: BWX_XXXXXXXXXXXXXXXX' \
  -d '{"page":1,"limit":10,"country":"Thailand"}'
```

---

## 8. Postman

Import `docs/Activity_BW_Out_API.postman_collection.json`, set `baseUrl` and
`api_key` in the collection variables, and run the folders in order. Reserve
Booking stores `booking_ref_no` and the held price into collection variables, so
Create Booking (confirm), Booking Details, Cancellation Charges and Cancel
Booking all run without editing anything.

---

## 9. Support

Quote the **`X-Request-Id`** of the failing call, and the `booking_ref_no` where
there is one. Every request on this API is logged against that id.
