Skip to main content

VAS Admin API

All endpoints below live in admin-api and are mounted at both /api/v1/admin/... and /api/v1/pms/.... They require an Auth0 JWT.

SurfaceBase pathController
VAS catalog/vasVasController
VAS catalog pricing (vas_cost)/vas-costsVasCostController
Channel-level VAS availability/channel-mappings/vasChannelMappingController
Listing-channel VAS overrides/listing-channel-mappings/vasListingChannelMappingController
Surface coverage

The new vas_variant, vas_bundle_item, vas_choice_group, vas_choice_option DAOs are wired but their REST controllers are not yet shipped. The JSON shapes below describe how the records will be exposed when the controllers land.


1. VAS Catalog — /vas

GET /vas — list catalog

GET /api/v1/admin/vas
[
{
"id": "BBQ_2V_2NV",
"name": "BBQ (2 Veg & 2 Non-Veg)",
"description": "Live BBQ counter with 2 veg + 2 non-veg picks per guest.",
"category": "FOOD",
"kind": "SINGLE",
"attributes": {
"category": "FOOD",
"mealType": "DINNER",
"vegCount": 2,
"nonVegCount": 2,
"cuisines": ["Indian"]
},
"constraints": { "minPax": 4, "leadTimeHours": 24 },
"createdAt": "2026-05-12T10:00:00Z",
"updatedAt": "2026-05-12T10:00:00Z"
}
]

GET /vas/{id} — fetch one

GET /api/v1/admin/vas/PREMIUM_SEDAN

Response carries the full Vas object including typed attributes. Returns 404 if missing.

POST /vas — upsert

Body is the full Vas. Existing IDs are overwritten. Validation: id and name must be non-blank.

Example A: FOOD SINGLE — BBQ (2 Veg & 2 Non-Veg)

POST /api/v1/admin/vas
Content-Type: application/json
{
"id": "BBQ_2V_2NV",
"name": "BBQ (2 Veg & 2 Non-Veg)",
"description": "Live BBQ counter with 2 veg + 2 non-veg picks per guest.",
"category": "FOOD",
"kind": "SINGLE",
"attributes": {
"category": "FOOD",
"mealType": "DINNER",
"vegCount": 2,
"nonVegCount": 2,
"cuisines": ["Indian"]
},
"constraints": {
"minPax": 4,
"leadTimeHours": 24
}
}

Example B: EXPERIENCE SINGLE — Rain Dance

{
"id": "RAIN_DANCE",
"name": "Rain Dance",
"category": "EXPERIENCE",
"kind": "SINGLE",
"attributes": {
"category": "EXPERIENCE",
"experienceType": "OTHER",
"durationMinutes": 90,
"isOutdoor": true,
"location": "POOLSIDE"
},
"constraints": {
"minPax": 6,
"leadTimeHours": 48,
"seasons": ["SUMMER", "MONSOON"]
}
}

Example C: TRANSPORT VARIANT_PARENT — Premium Sedan

{
"id": "PREMIUM_SEDAN",
"name": "Premium Sedan",
"category": "TRANSPORT",
"kind": "VARIANT_PARENT",
"attributes": {
"category": "TRANSPORT",
"vehicleClass": "SEDAN",
"vehicleModel": "SWIFT_DZIRE",
"vehicleLabel": "Premium Sedan - Swift Dzire",
"maxPassengers": 4,
"driverIncluded": true,
"fuelIncluded": true
}
}

Example D: BUNDLE (ROLLUP) — Weekend Night Package

{
"id": "WEEKEND_NIGHT_PACKAGE",
"name": "Movie Night + Bonfire + BBQ + High Tea",
"category": "EXPERIENCE",
"kind": "BUNDLE",
"bundlePricingMode": "ROLLUP",
"attributes": {
"category": "EXPERIENCE",
"experienceType": "OTHER",
"setupNotes": "Combined evening package"
}
}

After upserting the bundle parent, add children via the (forthcoming) bundle-item endpoint — see §6 Bundle Items.

Example E: CHEF — Chef (Grocery on Actual)

{
"id": "CHEF_GROCERY_ACTUAL",
"name": "Chef (Grocery on Actual)",
"category": "CHEF",
"kind": "SINGLE",
"attributes": {
"category": "CHEF",
"chefType": "PERSONAL",
"cuisines": ["Indian", "Continental"],
"durationHours": 10,
"mealCount": 3,
"groceryOnActuals": true
}
}

DELETE /vas/{id} — remove

DELETE /api/v1/admin/vas/RAIN_DANCE

Cascades to vas_variant, vas_bundle_item (as parent), vas_choice_group, vas_cost, channel_value_added_service, listing_channel_value_added_service. Returns 204 on success, 404 if missing.


2. VAS Catalog Pricing — /vas-costs

Manages vas_cost rows. Updates are propagated to listing_channel_value_added_service via FK so downstream pricing stays consistent.

POST /vas-costs — upsert one row

POST /api/v1/admin/vas-costs
Content-Type: application/json

FIXED

{
"vasId": "BONFIRE",
"tagName": "goa-peak",
"price": 2500.00,
"pricingType": "FIXED"
}

PER_PERSON

{
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 800.00,
"pricingType": "PER_PERSON"
}

BASE_PLUS_OVERAGE (variant-pinned)

{
"vasId": "PREMIUM_SEDAN",
"variantId": "SWIFT_DZIRE_4H_40KM",
"tagName": "goa-peak",
"price": 1800.00,
"pricingType": "BASE_PLUS_OVERAGE",
"pricingConfig": {
"type": "BASE_PLUS_OVERAGE",
"baseHours": 4,
"baseKm": 40,
"perExtraHour": 200.00,
"perExtraKm": 12.00
}
}

TIERED

{
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 800.00,
"pricingType": "TIERED",
"pricingConfig": {
"type": "TIERED",
"tiers": [
{ "fromUnits": 1, "toUnitsInclusive": 4, "pricePerUnit": 800.00 },
{ "fromUnits": 5, "toUnitsInclusive": 10, "pricePerUnit": 700.00 },
{ "fromUnits": 11, "toUnitsInclusive": null, "pricePerUnit": 600.00 }
]
}
}

ON_ACTUALS

{
"vasId": "CHEF_GROCERY_ACTUAL",
"tagName": "goa-peak",
"price": 0.00,
"pricingType": "ON_ACTUALS",
"pricingConfig": {
"type": "ON_ACTUALS",
"deposit": 0.00,
"markupPercent": 10
}
}

POST /vas-costs/bulk — bulk upsert

Body is an array of VasCost. All rows applied in a single transaction.

[
{ "vasId": "BONFIRE", "tagName": "goa-peak", "price": 2500.00, "pricingType": "FIXED" },
{ "vasId": "BONFIRE", "tagName": "goa-off-peak", "price": 1800.00, "pricingType": "FIXED" },
{ "vasId": "HIGH_TEA","tagName": "goa-peak", "price": 400.00, "pricingType": "PER_PERSON" }
]

CSV upload & export

MethodPathDescription
POST/vas-costs/upload-csvMultipart upload; field name file.
GET/vas-costs/exportStreams vas_costs.csv.
GET/vas-costs/sampleStreams a template CSV.

CSV header: VASID,TAGNAME,PRICE,PRICINGTYPE. (Structured pricing_config is not expressible via CSV — use JSON for those rows.)


3. Channel VAS Availability — /channel-mappings/vas

Marks a VAS available on a channel and lets the channel override price / pricing-type / structured config. Keyed by (channelId, vasId, tagName).

GET /channel-mappings/vas?channelId=...

GET /api/v1/admin/channel-mappings/vas?channelId=CH-BOOKING
[
{
"channelId": "CH-BOOKING",
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 850.00,
"pricingType": "PER_PERSON",
"isEnabled": true
}
]

GET /channel-mappings/vas/{channelId}/{vasId}/{tagName}

Returns the single row or 404.

POST /channel-mappings/vas — upsert

{
"channelId": "CH-BOOKING",
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 850.00,
"pricingType": "PER_PERSON",
"isEnabled": true
}

Override with a TIERED pricingConfig

{
"channelId": "CH-BOOKING",
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 800.00,
"pricingType": "TIERED",
"pricingConfig": {
"type": "TIERED",
"tiers": [
{ "fromUnits": 1, "toUnitsInclusive": 4, "pricePerUnit": 800.00 },
{ "fromUnits": 5, "toUnitsInclusive": null, "pricePerUnit": 700.00 }
]
},
"isEnabled": true
}

DELETE /channel-mappings/vas/{channelId}/{vasId}/{tagName}

Returns 204 on success, 404 if missing.


4. Listing-Channel VAS Override — /listing-channel-mappings/vas

Per-listing pricing override. Keyed by (listingId, channelId, vasId). Variant context (when applicable) is pulled through vasCostId rather than duplicated on the override row.

GET /listing-channel-mappings/vas/by-listing/{listingId}

[
{
"listingId": "L-1001",
"channelId": "CH-BOOKING",
"vasId": "BBQ_2V_2NV",
"vasCostId": 42,
"price": 900.00,
"pricingType": "PER_PERSON",
"isEnabled": true
}
]

GET /listing-channel-mappings/vas/by-channel/{channelId}

Same shape, listed for one channel across all listings.

GET /listing-channel-mappings/vas/{listingId}/{channelId}/{vasId}

Single row or 404.

POST /listing-channel-mappings/vas

{
"listingId": "L-1001",
"channelId": "CH-BOOKING",
"vasId": "BBQ_2V_2NV",
"vasCostId": 42,
"price": 900.00,
"pricingType": "PER_PERSON",
"isEnabled": true
}

Override with BASE_PLUS_OVERAGE config

{
"listingId": "L-1001",
"channelId": "CH-BOOKING",
"vasId": "PREMIUM_SEDAN",
"vasCostId": 87,
"price": 2000.00,
"pricingType": "BASE_PLUS_OVERAGE",
"pricingConfig": {
"type": "BASE_PLUS_OVERAGE",
"baseHours": 4,
"baseKm": 40,
"perExtraHour": 250.00,
"perExtraKm": 14.00
},
"isEnabled": true
}

DELETE /listing-channel-mappings/vas/{listingId}/{channelId}/{vasId}

Returns 204 on success.


5. Variants — /vas-variants (planned)

VasVariantDao is wired; the REST surface will live at /vas-variants.

GET /vas-variants?vasId=...

[
{
"id": "SWIFT_DZIRE_4H_40KM",
"vasId": "PREMIUM_SEDAN",
"name": "Swift Dzire - 4 Hours / 40 KMs",
"attributes": {
"category": "TRANSPORT",
"vehicleClass": "SEDAN",
"vehicleModel": "SWIFT_DZIRE",
"baseHours": 4,
"baseKm": 40
},
"sortOrder": 10,
"isEnabled": true
}
]

POST /vas-variants — upsert

{
"id": "SWIFT_DZIRE_8H_80KM",
"vasId": "PREMIUM_SEDAN",
"name": "Swift Dzire - 8 Hours / 80 KMs",
"attributes": {
"category": "TRANSPORT",
"vehicleClass": "SEDAN",
"vehicleModel": "SWIFT_DZIRE",
"baseHours": 8,
"baseKm": 80
},
"sortOrder": 20,
"isEnabled": true
}

6. Bundle Items — /vas-bundle-items (planned)

VasBundleItemDao exposes insert / update / delete / findByParent / deleteByParent.

GET /vas-bundle-items?parentVasId=WEEKEND_NIGHT_PACKAGE

[
{ "id": 1, "parentVasId": "WEEKEND_NIGHT_PACKAGE", "childVasId": "MOVIE_NIGHT", "quantity": 1, "isOptional": false, "defaultSelected": true, "sortOrder": 10 },
{ "id": 2, "parentVasId": "WEEKEND_NIGHT_PACKAGE", "childVasId": "BONFIRE", "quantity": 1, "isOptional": false, "defaultSelected": true, "sortOrder": 20 },
{ "id": 3, "parentVasId": "WEEKEND_NIGHT_PACKAGE", "childVasId": "BBQ_VEG", "quantity": 1, "isOptional": false, "defaultSelected": true, "sortOrder": 30 },
{ "id": 4, "parentVasId": "WEEKEND_NIGHT_PACKAGE", "childVasId": "BBQ_NONVEG", "quantity": 1, "isOptional": false, "defaultSelected": true, "sortOrder": 40 },
{ "id": 5, "parentVasId": "WEEKEND_NIGHT_PACKAGE", "childVasId": "HIGH_TEA", "quantity": 1, "isOptional": true, "defaultSelected": true, "sortOrder": 50 }
]

POST /vas-bundle-items — insert

{
"parentVasId": "AIRPORT_TRANSFER_PACKAGE",
"childVasId": "PREMIUM_SEDAN",
"childVariantId": "SWIFT_DZIRE_4H_40KM",
"quantity": 2,
"isOptional": false,
"defaultSelected": true,
"sortOrder": 10
}

DELETE /vas-bundle-items/{id}

Returns 204.


7. Choice Groups — /vas-choice-groups (planned)

GET /vas-choice-groups?vasId=BBQ_PICK_YOUR_ITEMS

[
{ "id": 11, "vasId": "BBQ_PICK_YOUR_ITEMS", "code": "VEG_ITEMS", "name": "Vegetarian picks", "minSelect": 2, "maxSelect": 2, "sortOrder": 10 },
{ "id": 12, "vasId": "BBQ_PICK_YOUR_ITEMS", "code": "NON_VEG_ITEMS", "name": "Non-vegetarian picks", "minSelect": 2, "maxSelect": 2, "sortOrder": 20 }
]

POST /vas-choice-groups — upsert (keyed by (vasId, code))

{
"vasId": "BBQ_PICK_YOUR_ITEMS",
"code": "ADD_ONS",
"name": "Extras",
"minSelect": 0,
"maxSelect": 3,
"sortOrder": 30
}

8. Choice Options — /vas-choice-options (planned)

GET /vas-choice-options?groupId=11

[
{ "id": 101, "groupId": 11, "code": "PANEER_TIKKA", "label": "Paneer Tikka", "extraPrice": 0.00, "isDefault": true, "sortOrder": 10 },
{ "id": 102, "groupId": 11, "code": "MUSHROOM_TIKKA", "label": "Mushroom Tikka", "extraPrice": 0.00, "isDefault": true, "sortOrder": 20 },
{ "id": 103, "groupId": 11, "code": "PANEER_ACHARI", "label": "Paneer Achari", "extraPrice": 50.00, "isDefault": false, "sortOrder": 30 }
]

POST /vas-choice-options — upsert (keyed by (groupId, code))

{
"groupId": 12,
"code": "MUTTON_SEEKH",
"label": "Mutton Seekh",
"extraPrice": 150.00,
"isDefault": false,
"sortOrder": 30
}

End-to-end: onboarding the Weekend Night Package

A worked sequence that touches most surfaces:

  1. Upsert leaf VAS rows (MOVIE_NIGHT, BONFIRE, BBQ_VEG, BBQ_NONVEG, HIGH_TEA) → POST /vas.
  2. Upsert the bundle parent (WEEKEND_NIGHT_PACKAGE, kind=BUNDLE, bundlePricingMode=ROLLUP) → POST /vas.
  3. Wire bundle childrenPOST /vas-bundle-items (5 calls).
  4. Set baseline prices for each leaf → POST /vas-costs/bulk.
  5. Set the rollup price for the bundle → POST /vas-costs with vasId = WEEKEND_NIGHT_PACKAGE, pricingType = FIXED, price = 12000.
  6. Enable on a channelPOST /channel-mappings/vas (one call per leaf + the bundle, with isEnabled=true).
  7. Optionally override for a specific listing → POST /listing-channel-mappings/vas.

The booking pipeline will then resolve the package end-to-end: when a guest adds WEEKEND_NIGHT_PACKAGE, it picks up the listing-channel override (or falls back to the channel row, then vas_cost) and renders the bundle children.

Error Responses

All endpoints share the standard ErrorResponse envelope:

{
"status": 400,
"error": "Bad Request",
"message": "id must not be blank",
"timestamp": "2026-05-15T10:23:11Z",
"path": "/api/v1/admin/vas"
}
StatusWhen
400Validation error (blank id/name, bad enum, FK violation surfaced by Spring)
404Path-id lookup miss
409Currently surfaces as 400 on unique-constraint conflict
500Unexpected server error (CSV I/O, DB outage)