Using a Coupon at Checkout
The checkout payload does not change when a coupon is used — you only add two keys to it. This is the same for every product that goes through the unified booking endpoints, not hotels only.
The Keys
| Key | Type | Where | Required | Description |
|---|---|---|---|---|
coupon_code | String | Top level | Optional | The coupon the user wants to use. Omit it to book at full price. |
user_id | Integer | Top level | Conditional | The signed-in user's ID. Required for any coupon the user claimed. |
{
"user_id": 3312,
"coupon_code": "SONGKRAN10"
}
Top level, not inside
products. Puttingcoupon_codeoruser_idinside aproductsitem has no effect — the coupon is silently ignored and the customer pays full price.
Where to Send Them
Add the same two keys to both calls of the booking flow. Everything else stays exactly as documented for the product — for hotels, Hotel Checkout and Hotel Checkout Confirm.
| Step | Endpoint | Send the keys? |
|---|---|---|
| 1. Checkout | POST /api/v1/unified-booking/checkout | Yes |
| 2. Confirm | POST /api/v1/unified-booking/checkout/confirm | Yes — the same code again |
| 3. Payment | Payment Request API | No — nothing coupon-specific |
Which Products Support It
The two keys are identical for every product type. Only product_type inside products changes.
product_type | Product | Coupon support |
|---|---|---|
pd11 | Hotel | Full |
pd4 | More Experiences | Full |
pd6 | Airport Transfer | Full |
pd7 | Airport VIP Assistance | Full |
pd8 | Tour | Full |
pd9 | Flight | Limited — see below |
What Comes Back
The data array is unchanged. The discount appears in the checkout response's meta:
meta field | Meaning |
|---|---|
coupon_code | Echo of the code that was applied. |
discount_amount | The saving, capped at sub_total. |
sub_total | Cart total before the discount. |
grand_total | sub_total − discount_amount. The bank fee is calculated on this. |
bank_fee_price | Fee for the payment method, charged on the discounted amount. |
payable | grand_total + bank_fee_price — what the customer pays. |
{
"meta": {
"currency_code": "THB",
"sub_total": 5000,
"coupon_code": "SONGKRAN10",
"discount_amount": 500,
"grand_total": 4500,
"bank_fee_price": 135,
"payable": 4635
}
}
The discount is applied before the bank fee, so a coupon lowers the fee too. On confirm, the
returned grand_total is already the final payable amount — use it for the Payment Request API.
Coupon Status Along the Way
| Stage | Coupon status |
|---|---|
| Checkout | Unchanged — validated and priced only, nothing is written. |
| Confirm | reserved, held against the new booking. |
| Payment success | redeemed. Final. |
| Payment failed / cancelled, or unpaid for 30 minutes | Released back to claimed. |
When the Coupon Is Rejected
An unusable coupon rejects the whole checkout — you do not get a booking at full price. The response is the standard envelope with HTTP 200:
{
"result": 0,
"message": "Grand total does not meet the minimum purchase amount."
}
| Message | What to do |
|---|---|
Coupon code not found. | The code does not exist, or is not usable by this user or partner. |
This coupon does not belong to the requested user. | Send the owner's user_id, or drop the code. |
This coupon is not valid | Wrong product type, or user_id missing on a members-only coupon. |
The coupon has expired. | Ask the user to pick another. |
The claim duration for this coupon has expired. | Claimed but not used in time. |
Grand total does not meet the minimum purchase amount. | Cart is below the coupon's minimum spend. |
You already have a pending booking with this coupon. | Finish or cancel that booking first. |
The usage limit per user for this campaign has been reached. | The user already redeemed their allowance. |
The campaign usage limit has been reached. | The coupon's global limit is exhausted. |
To let the booking go through anyway, retry the same request without coupon_code.
Notes
- One coupon per booking.
- For hotels the property is resolved from
hotel_code, so hotel- and destination-restricted coupons need no extra fields. - Amounts follow your
X-Currencyheader. Readmeta.discount_amountandmeta.payablefrom the response instead of calculating the discount yourself — the coupon is priced against the live rate. - Call Applicable Coupons before checkout to offer only the coupons that fit the cart, so a rejection here becomes rare.