Rate Management
Use these endpoints to manage your rates from your own site or back office: create rate plans, layer seasonal pricing, bundle inclusions and credits, and run promotions with codes. The catalog and offer endpoints only read; these write.
Every endpoint here needs the rates:write scope, except the GET reads, which need rates:read. Updating a plan, or changing its intervals, queues a rate update to your connected channels.
A promotion is a rate plan
A promotion is a rate plan with kind: "promotion" and a derivation from a base
plan. There is no separate promotion endpoint: you create and edit one through the
same /rate-plans endpoints, setting kind, derived_from_id, derivation_type,
and derivation_value.
Idempotency
Every write here is a mutating request. Send an Idempotency-Key header to make
retries safe, as described in Conventions.
Create a rate plan
POST /rate-plans| Field | Required | Notes |
|---|---|---|
room_type_id | yes | The room type this plan prices. |
name | yes | Internal name, 1 to 100 characters. |
base_price | yes | Nightly price as a decimal string, for example "120.00". |
currency | no | Must be the property's base currency or a configured currency. Defaults to base. |
public_name | no | Guest-facing name, up to 200 characters. |
description | no | Guest-facing description, up to 2,000 characters. |
image_url | no | Absolute URL. |
extra_guest_charge | no | Per-night charge per guest above base occupancy. Defaults to "0.00". |
single_guest_discount | no | Defaults to "0.00". |
single_guest_discount_type | no | fixed or percent. Defaults to fixed. |
min_los / max_los | no | Length-of-stay bounds in nights, 1 to 365. |
booking_window_start / _end | no | When the plan is on sale (YYYY-MM-DD). |
stay_window_start / _end | no | When the stay may fall (YYYY-MM-DD). |
active_for_booking_engine | no | Defaults to false. |
active_for_channels | no | Defaults to false. |
requires_promo_code | no | true hides the plan from listings and makes it bookable only with one of its promo codes. Defaults to false. |
kind | no | standard (default) or promotion. |
derived_from_id | no | For a promotion: the base plan it derives from. |
derivation_type | no | percent, fixed, or perPerson. Required with derived_from_id. |
derivation_value | no | Signed decimal string. Negative is a discount, for example "-20.00". Required with derived_from_id. |
curl -X POST "$VRDN_BASE/rate-plans" \
-H "Authorization: Bearer $VRDN_KEY" \
-H "Content-Type: application/json" \
-d '{
"room_type_id": "9b2f6c3e-8d1a-4f5b-a7c9-4e0d2b8f1a63",
"name": "Bed & Breakfast",
"base_price": "120.00",
"public_name": "Bed & Breakfast",
"description": "A full breakfast for two each morning.",
"active_for_booking_engine": true
}'Returns 201:
{ "rate_plan_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02" }An unknown room_type_id returns 404 room_type_not_found.
Create a promotion
Set kind, the parent, and a negative derivation. Gate it behind a code by adding one (see below) and setting requires_promo_code: true.
curl -X POST "$VRDN_BASE/rate-plans" \
-H "Authorization: Bearer $VRDN_KEY" \
-H "Content-Type: application/json" \
-d '{
"room_type_id": "9b2f6c3e-8d1a-4f5b-a7c9-4e0d2b8f1a63",
"name": "Advance Purchase, save 20%",
"base_price": "120.00",
"kind": "promotion",
"derived_from_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02",
"derivation_type": "percent",
"derivation_value": "-20.00",
"requires_promo_code": true,
"active_for_booking_engine": true
}'A derived plan needs both derivation_type and derivation_value, or the request is rejected. Its price is always the parent's current price with the derivation applied, so it follows the parent's seasonal pricing automatically.
Update a rate plan
PATCH /rate-plans/{id}Send only the fields you want to change. Accepts the same fields as create (except room_type_id), plus is_active.
{ "rate_plan_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02", "updated": true }Delete a rate plan
DELETE /rate-plans/{id}The default rate plan cannot be deleted (403 cannot_delete_default). An unknown id returns 404 rate_plan_not_found. Reservations already made on a deleted plan keep the price they were quoted.
{ "rate_plan_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02", "deleted": true }Seasonal intervals
Intervals layer date-specific pricing and stay rules over a plan's base price. Intervals on the same plan must not overlap; an overlapping range returns 409 interval_overlap.
List intervals
GET /rate-plans/{id}/intervalsRequires rates:read. Returns { "data": [...] }, one entry per interval with its id, rate_plan_id, and every field below.
Create an interval
POST /rate-plans/{id}/intervals| Field | Required | Notes |
|---|---|---|
name | yes | 1 to 100 characters, for example "High Season". |
start_date / end_date | yes | YYYY-MM-DD. Start must be before end. |
price_mon to price_sun | no | Per-weekday price. A day you leave out falls back to the base price. |
min_stay / max_stay | no | Length-of-stay rules for this range, at least 1. min_stay cannot exceed max_stay. |
closed_to_arrival / closed_to_departure / stop_sell | no | Availability flags. Default false. |
extra_guest_charge / single_guest_discount | no | Per-season overrides. Default "0.00". |
curl -X POST "$VRDN_BASE/rate-plans/5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02/intervals" \
-H "Authorization: Bearer $VRDN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "High Season",
"start_date": "2026-12-01",
"end_date": "2027-03-31",
"price_mon": "180.00", "price_fri": "210.00",
"min_stay": 3
}'Returns 201:
{ "interval_id": "d8a4c2f6-5e9b-4d7a-8f1c-3b6e0a9d5c28" }Update and delete an interval
PATCH /intervals/{id}
DELETE /intervals/{id}PATCH accepts any interval field and checks the non-overlap rule again. Both return 404 interval_not_found for an unknown id.
Inclusions and credits
An inclusion is a perk (a fixed included item) or a credit (a spendable allowance for a category, such as a spa credit). You set inclusions as a whole list: a POST replaces the plan's entire set, up to 30 entries. Send an empty list to remove them all.
List inclusions
GET /rate-plans/{id}/inclusionsRequires rates:read. Returns { "data": [...] }, one entry per inclusion with its id and the fields below.
Replace inclusions
POST /rate-plans/{id}/inclusions| Field | Required | Notes |
|---|---|---|
label | yes | Guest-facing label, up to 120 characters, for example "Spa credit". |
allocated_amount | yes | Value as a decimal string. "0.00" for a free perk. |
frequency | yes | perStay, perNight, perGuest, or perGuestPerNight. |
inclusion_kind | no | perk (default) or credit. |
revenue_category | yes | A category name of 1 to 40 characters, for example room, fnb, or spa. For a credit, this is the category it applies to. |
service_item_id | no | Link to a catalogue service item. |
currency | no | Defaults to the plan currency. |
included_quantity | no | 1 to 99. Defaults to 1. |
curl -X POST "$VRDN_BASE/rate-plans/5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02/inclusions" \
-H "Authorization: Bearer $VRDN_KEY" \
-H "Content-Type: application/json" \
-d '{
"inclusions": [
{ "label": "Daily breakfast for two", "allocated_amount": "20.00", "frequency": "perGuestPerNight", "revenue_category": "fnb" },
{ "label": "Spa credit", "allocated_amount": "150.00", "frequency": "perStay", "inclusion_kind": "credit", "revenue_category": "spa" }
]
}'{ "rate_plan_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02", "count": 2 }Promo codes
A code unlocks a promotion. The discount lives in the promotion, so a code never carries a saving of its own. Attach codes to the plan they unlock.
List codes
GET /rate-plans/{id}/promo-codesRequires rates:read. Returns { "data": [...] }, one entry per code with id, code, rate_plan_id, valid_from, valid_to, max_redemptions, redemption_count, max_per_guest, and is_active.
Create a code
POST /rate-plans/{id}/promo-codes| Field | Required | Notes |
|---|---|---|
code | yes | 3 to 40 characters. Trimmed and stored in upper case, and must be unique in the property (409 code_exists otherwise). |
valid_from / valid_to | no | YYYY-MM-DD window. valid_to cannot be before valid_from. |
max_redemptions | no | Total-use cap. Omit for unlimited. |
max_per_guest | no | Per-guest cap. Omit for unlimited. |
curl -X POST "$VRDN_BASE/rate-plans/e2a9d7c4-3f6b-4c1e-9a8d-7b5f0c2e4a19/promo-codes" \
-H "Authorization: Bearer $VRDN_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "EARLYBIRD25", "max_redemptions": 100 }'Returns 201:
{ "promo_code_id": "a5e2c8f1-7d4b-4a6e-b9c3-0f8d2a5e7c41", "code": "EARLYBIRD25" }Update and delete a code
PATCH /promo-codes/{id}
DELETE /promo-codes/{id}PATCH accepts valid_from, valid_to, max_redemptions, max_per_guest, and is_active. The code string itself cannot be changed, because reservations freeze the code they were made with; delete it and create a new one instead. Deleting a code does not affect reservations already made with it. Both return 404 promo_code_not_found for an unknown id.