Discounts & Coupons
Complete guide for discount rules and coupon validation.
Overview
LosCenotes supports two types of discounts:
- Automatic Discounts - Applied automatically based on eligibility rules
- Coupon Codes - Applied manually by entering a code
Automatic Discount Rules
GET /public/discount-rules/auto-apply
Get all active automatic discount rules.
curl -X GET "https://service-gateway.loscenotes.com/public/discount-rules/auto-apply" \
-H "Content-Type: application/json"
Response:
{
"success": true,
"message": "discount_rule.list_retrieved_successfully",
"data": [
{
"id": "discount-uuid-1",
"name": "Descuento de Temporada Baja",
"description": "10% de descuento en todos los cenotes durante temporada baja",
"discountType": "percentage",
"appliesTo": "cenote",
"value": 10,
"minAmount": 10000,
"maxDiscountAmount": 50000,
"currency": "MXN",
"priority": 10,
"stackable": false,
"autoApply": true,
"excludeTransportFromDiscount": true,
"userEligibility": "all",
"isActive": true,
"promotion": {
"id": "promo-uuid",
"name": "Temporada Baja 2025",
"slug": "temporada-baja-2025",
"status": "active",
"promotionType": "seasonal",
"startDate": "2025-05-01T00:00:00.000Z",
"endDate": "2025-06-30T23:59:59.000Z"
}
},
{
"id": "discount-uuid-2",
"name": "Descuento Primera Compra",
"description": "15% de descuento en tu primera reservación",
"discountType": "percentage",
"appliesTo": "all",
"value": 15,
"minAmount": null,
"maxDiscountAmount": 100000,
"currency": "MXN",
"priority": 5,
"stackable": false,
"autoApply": true,
"excludeTransportFromDiscount": false,
"userEligibility": "first_purchase",
"isActive": true
}
]
}
Discount Types
| Type | Description | Example |
|---|---|---|
percentage | Percentage off total | 10% off → value: 10 |
fixed | Fixed amount off | $50 off → value: 5000 (cents) |
quantity_based | Based on quantity purchased | Buy 3, get 1 free |
User Eligibility
| Eligibility | Description |
|---|---|
all | All users |
authenticated | Registered users only |
vip | VIP members only |
first_purchase | First-time buyers |
local_resident | Local residents (Quintana Roo) |
mexican_national | Mexican nationals |
Applies To
| Value | Description |
|---|---|
all | All items (cenotes and tours) |
cenote | Cenotes only |
tour | Tours only |
specific | Specific items (see promotion metadata) |
Coupon Validation
POST /coupons/validate
Validate a coupon code before applying it.
curl -X POST "https://service-gateway.loscenotes.com/coupons/validate" \
-H "Content-Type: application/json" \
-d '{
"code": "VERANO2025"
}'
Request Fields:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | ✅ | Coupon code to validate |
Success Response:
{
"success": true,
"message": "coupons.validation_completed",
"data": {
"isValid": true,
"message": "Coupon is valid and can be applied",
"coupon": {
"id": "coupon-uuid",
"code": "VERANO2025",
"name": "Descuento Verano 2025",
"discountType": "percentage",
"discountValue": 10,
"minPurchaseAmount": 20000,
"maxDiscountAmount": 50000,
"usageLimit": 1000,
"usageCount": 245,
"validFrom": "2025-06-01T00:00:00.000Z",
"validUntil": "2025-08-31T23:59:59.000Z",
"isActive": true
},
"potentialDiscount": 10000,
"errors": []
},
"currency": {
"code": "MXN",
"symbol": "$"
}
}
Invalid Response:
{
"success": true,
"message": "coupons.validation_completed",
"data": {
"isValid": false,
"message": "Coupon has expired",
"coupon": null,
"potentialDiscount": null,
"errors": ["Coupon has expired"]
}
}
POST /coupons/check
Check if a coupon code exists and is available (alias for validate).
curl -X POST "https://service-gateway.loscenotes.com/coupons/check" \
-H "Content-Type: application/json" \
-d '{
"code": "VERANO2025"
}'
Using Coupons in Pricing
Include the coupon code when calling /pricing/calculate-complete:
{
"itemType": "cenote",
"itemId": "cenote-uuid",
"ageBreakdown": {
"adult": 2
},
"couponCode": "VERANO2025"
}
The response will show the coupon discount in the discounts section:
{
"discounts": {
"automatic": [],
"coupon": {
"code": "VERANO2025",
"type": "percentage",
"amount": 7000
}
},
"summary": {
"subtotal": 70000,
"discounts": 7000,
"total": 63000
}
}
Discount Stacking
Stackable Discounts
If stackable: true, the discount can be combined with other discounts.
Non-Stackable Discounts
If stackable: false, only the highest priority discount will be applied.
Priority Order:
- Higher
priorityvalue wins - If same priority, automatic discounts apply before coupons
Transport Exclusion
When excludeTransportFromDiscount: true:
- Discount applies only to: Base price + Optional services
- Discount does NOT apply to: Transportation costs
Note: This option is only available for
percentagetype discounts.
Coupon Validation Errors
| Error | Description |
|---|---|
Coupon not found | Code doesn't exist |
Coupon has expired | Past valid date |
Coupon not yet valid | Before start date |
Coupon usage limit reached | Max uses exceeded |
Coupon is inactive | Manually deactivated |
Minimum purchase not met | Below minimum amount |
Best Practices
1. Always Validate First
Before showing discount in UI, validate the coupon:
// Step 1: Validate coupon
const validation = await fetch('/coupons/validate', {
method: 'POST',
body: JSON.stringify({ code: userInput })
});
// Step 2: Only if valid, calculate price with coupon
if (validation.data.isValid) {
const pricing = await fetch('/pricing/calculate-complete', {
method: 'POST',
body: JSON.stringify({
itemId: cenoteId,
ageBreakdown: { adult: 2 },
couponCode: userInput
})
});
}
2. Display Savings
Show users how much they're saving:
const { discounts, summary } = pricingResponse.data;
const totalSavings = discounts.discounts; // In cents
console.log(`You save: $${(totalSavings / 100).toFixed(2)} MXN`);
3. Handle Auto-Apply
Automatic discounts are applied automatically. Check the response:
const autoDiscounts = pricingResponse.data.discounts.automatic;
if (autoDiscounts.length > 0) {
autoDiscounts.forEach(d => {
console.log(`Applied: ${d.name} - Save $${d.amount / 100}`);
});
}