Get Public Offers
Get available loyalty offers without authentication
/public/offersOverview
Returns publicly available loyalty offers that don't require member authentication. These are typically promotional offers, global campaigns, or property-specific offers that can be viewed by anyone to understand the Journey loyalty program benefits.
Authentication
No authentication required. This is a public endpoint.
Query Parameters
propertyIdquerystringFilter offers by property document ID
categoryIdquerynumberFilter offers by category ID
redemptionTypequerystringFilter by redemption type (e.g., "dining", "spa", "room_upgrade")
limitquerynumberdefault: 20Maximum number of offers to return (1-100)
offsetquerynumberdefault: 0Number of offers to skip for pagination
Request
curl -X GET "https://api.journey.com/v1/public/offers?limit=10&offset=0"Response
itemsarrayrequiredArray of available offers
offer items
offerIdstringrequiredUnique offer identifier
namestringrequiredOffer name/title
shortDescriptionstringBrief description for preview/cards
descriptionstringFull offer description
pointsRequirednumberrequiredPoints required to redeem this offer
retailValuenumberRetail value of the offer in USD
discountAmountnumberDiscount amount in USD (if applicable)
discountTypestringType of discount (percentage, fixed_amount, etc.)
redemptionTypestringrequiredHow the offer is redeemed (dining, spa, room_upgrade, etc.)
categoryobjectOffer category information
category object
idnumberrequiredCategory ID
namestringrequiredCategory name
heroImageUrlstringMain image URL for the offer
cardImageUrlstringCard/thumbnail image URL
howToRedeemstringInstructions for redeeming the offer
termsAndConditionsstringOffer terms and conditions
rulesstringAdditional rules and restrictions
isEligiblebooleanrequiredWhether this offer is generally eligible (always true for public offers)
restrictionsarrayrequiredActive restrictions that may apply
restriction items
typestringrequiredRestriction type
Options: requires_stay, check_in_window
messagestringrequiredHuman-readable restriction message
requiresStaybooleanrequiredWhether offer requires an active hotel stay to redeem
disableBeforeCheckInHoursnumberHours before check-in when offer becomes unavailable (null if no restriction)
validUntilstringOffer expiration date (ISO timestamp, null if no expiration)
totalnumberrequiredTotal number of offers matching the criteria
offsetnumberCurrent pagination offset
limitnumberCurrent pagination limit
Response
{
"items": [
{
"offerId": "offer_welcome_drink_2024",
"name": "Welcome Cocktail",
"shortDescription": "Complimentary signature cocktail at arrival",
"description": "Enjoy a complimentary signature cocktail crafted by our expert bartenders upon arrival. Choose from our selection of house specialties featuring local ingredients.",
"pointsRequired": 2500,
"retailValue": 18.00,
"discountAmount": null,
"discountType": null,
"redemptionType": "dining",
"category": {
"id": 1,
"name": "Food & Beverage"
},
"heroImageUrl": "https://cdn.journey.com/offers/welcome-cocktail-hero.jpg",
"cardImageUrl": "https://cdn.journey.com/offers/welcome-cocktail-card.jpg",
"howToRedeem": "Present your Journey membership at the main bar upon arrival",
"termsAndConditions": "Valid for new arrivals only. One per stay. Must be 21+ to redeem alcoholic beverages.",
"rules": "Cannot be combined with other bar promotions. Valid at main property bar only.",
"isEligible": true,
"restrictions": [
{
"type": "requires_stay",
"message": "Must have an active reservation to redeem"
}
],
"requiresStay": true,
"disableBeforeCheckInHours": 24,
"validUntil": "2024-12-31T23:59:59Z"
},
{
"offerId": "offer_spa_discount_spring",
"name": "Spring Spa Renewal",
"shortDescription": "20% off spa treatments",
"description": "Refresh and rejuvenate with 20% off all spa treatments during spring season. Includes massages, facials, and body treatments.",
"pointsRequired": 5000,
"retailValue": null,
"discountAmount": 20.00,
"discountType": "percentage",
"redemptionType": "spa",
"category": {
"id": 3,
"name": "Wellness & Spa"
},
"heroImageUrl": "https://cdn.journey.com/offers/spa-spring-hero.jpg",
"cardImageUrl": "https://cdn.journey.com/offers/spa-spring-card.jpg",
"howToRedeem": "Book spa services and mention this offer when scheduling",
"termsAndConditions": "Valid March 1 - May 31, 2024. Discount applies to treatment only, not products. Advance booking required.",
"rules": "Minimum $100 treatment required. Cannot be combined with other spa offers.",
"isEligible": true,
"restrictions": [
{
"type": "requires_stay",
"message": "Must be a registered hotel guest"
}
],
"requiresStay": true,
"disableBeforeCheckInHours": null,
"validUntil": "2024-05-31T23:59:59Z"
},
{
"offerId": "offer_room_upgrade_global",
"name": "Complimentary Room Upgrade",
"shortDescription": "Subject to availability at check-in",
"description": "Enjoy a complimentary room upgrade to the next category when available. Upgrade is confirmed at check-in based on availability.",
"pointsRequired": 7500,
"retailValue": 75.00,
"discountAmount": null,
"discountType": null,
"redemptionType": "room_upgrade",
"category": {
"id": 2,
"name": "Room & Accommodations"
},
"heroImageUrl": "https://cdn.journey.com/offers/room-upgrade-hero.jpg",
"cardImageUrl": "https://cdn.journey.com/offers/room-upgrade-card.jpg",
"howToRedeem": "Request upgrade at check-in and mention Journey membership",
"termsAndConditions": "Subject to availability. One upgrade per stay. Cannot upgrade from highest room category.",
"rules": "Not valid during peak season or special events. Blackout dates may apply.",
"isEligible": true,
"restrictions": [
{
"type": "requires_stay",
"message": "Must have confirmed reservation"
},
{
"type": "check_in_window",
"message": "Only available during check-in process"
}
],
"requiresStay": true,
"disableBeforeCheckInHours": 3,
"validUntil": null
}
],
"total": 15,
"offset": 0,
"limit": 20
}Use Cases
Public Offers Showcase
const PublicOffersShowcase = ({ propertyId }) => {
const [offers, setOffers] = useState([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
categoryId: null,
redemptionType: null
});
useEffect(() => {
const fetchOffers = async () => {
setLoading(true);
try {
const data = await getPublicOffers({
propertyId,
...filters,
limit: 12
});
setOffers(data.items);
} catch (error) {
console.error('Failed to fetch offers:', error);
} finally {
setLoading(false);
}
};
fetchOffers();
}, [propertyId, filters]);
const groupedOffers = offers.reduce((groups, offer) => {
const category = offer.category?.name || 'Other';
if (!groups[category]) groups[category] = [];
groups[category].push(offer);
return groups;
}, {});
const renderOffer = (offer) => (
<div key={offer.offerId} className="offer-card">
<div className="offer-image">
<img
src={offer.cardImageUrl || offer.heroImageUrl}
alt={offer.name}
onError={(e) => {
e.target.src = '/placeholder-offer.jpg';
}}
/>
{offer.requiresStay && (
<div className="requires-stay-badge">Requires Stay</div>
)}
</div>
<div className="offer-content">
<h3>{offer.name}</h3>
<p>{offer.shortDescription}</p>
<div className="offer-details">
<div className="points-required">
{offer.pointsRequired.toLocaleString()} points
</div>
{offer.retailValue && (
<div className="retail-value">
Value: ${offer.retailValue}
</div>
)}
{offer.discountAmount && (
<div className="discount">
{offer.discountType === 'percentage' ? `${offer.discountAmount}% off` : `$${offer.discountAmount} off`}
</div>
)}
</div>
<div className="offer-restrictions">
{offer.restrictions.map((restriction, index) => (
<span key={index} className="restriction-tag">
{restriction.message}
</span>
))}
</div>
{offer.validUntil && (
<div className="expiration">
Valid until {new Date(offer.validUntil).toLocaleDateString()}
</div>
)}
<button
className="view-offer-btn"
onClick={() => showOfferDetails(offer)}
>
Learn More
</button>
</div>
</div>
);
if (loading) return <div className="loading-offers">Loading offers...</div>;
return (
<div className="offers-showcase">
<div className="offers-header">
<h2>Available Offers</h2>
<p>Discover exclusive experiences with Journey points</p>
</div>
<div className="offers-filters">
<select
value={filters.categoryId || ''}
onChange={(e) => setFilters(prev => ({
...prev,
categoryId: e.target.value ? parseInt(e.target.value) : null
}))}
>
<option value="">All Categories</option>
<option value="1">Food & Beverage</option>
<option value="2">Room & Accommodations</option>
<option value="3">Wellness & Spa</option>
<option value="4">Recreation & Activities</option>
</select>
<select
value={filters.redemptionType || ''}
onChange={(e) => setFilters(prev => ({
...prev,
redemptionType: e.target.value || null
}))}
>
<option value="">All Types</option>
<option value="dining">Dining</option>
<option value="spa">Spa</option>
<option value="room_upgrade">Room Upgrade</option>
<option value="amenity_access">Amenities</option>
</select>
</div>
{Object.entries(groupedOffers).map(([category, categoryOffers]) => (
<div key={category} className="offer-category">
<h3>{category}</h3>
<div className="offers-grid">
{categoryOffers.map(renderOffer)}
</div>
</div>
))}
{offers.length === 0 && (
<div className="no-offers">
<h3>No offers available</h3>
<p>Check back soon for new exclusive offers!</p>
</div>
)}
</div>
);
};Offer Value Calculator
const OfferValueCalculator = ({ offer }) => {
const [memberTier, setMemberTier] = useState('bronze');
const calculateValue = () => {
const pointsValue = offer.pointsRequired * getPointValue(memberTier);
const savings = offer.retailValue ? (offer.retailValue - pointsValue) : 0;
const discountValue = offer.discountAmount || 0;
return {
pointsValue,
retailValue: offer.retailValue || 0,
totalValue: (offer.retailValue || 0) + discountValue,
savings: Math.max(savings, 0),
worthIt: savings > 0 || discountValue > pointsValue
};
};
const getPointValue = (tier) => {
const values = {
bronze: 0.01, // 1 cent per point
silver: 0.012, // 1.2 cents per point
gold: 0.015, // 1.5 cents per point
platinum: 0.018 // 1.8 cents per point
};
return values[tier] || values.bronze;
};
const value = calculateValue();
return (
<div className="offer-value-calculator">
<h4>Offer Value Analysis</h4>
<div className="tier-selector">
<label>Your membership tier:</label>
<select value={memberTier} onChange={(e) => setMemberTier(e.target.value)}>
<option value="bronze">Bronze</option>
<option value="silver">Silver</option>
<option value="gold">Gold</option>
<option value="platinum">Platinum</option>
</select>
</div>
<div className="value-breakdown">
<div className="value-row">
<span>Points required:</span>
<span>{offer.pointsRequired.toLocaleString()}</span>
</div>
<div className="value-row">
<span>Point value ({memberTier}):</span>
<span>${value.pointsValue.toFixed(2)}</span>
</div>
{offer.retailValue && (
<div className="value-row">
<span>Retail value:</span>
<span>${offer.retailValue.toFixed(2)}</span>
</div>
)}
{offer.discountAmount && (
<div className="value-row">
<span>Additional discount:</span>
<span>
{offer.discountType === 'percentage'
? `${offer.discountAmount}%`
: `$${offer.discountAmount}`}
</span>
</div>
)}
<div className="value-row total">
<span>Total value:</span>
<span>${value.totalValue.toFixed(2)}</span>
</div>
{value.savings > 0 && (
<div className="value-row savings">
<span>Your savings:</span>
<span>${value.savings.toFixed(2)}</span>
</div>
)}
</div>
<div className={`value-verdict ${value.worthIt ? 'good-deal' : 'poor-deal'}`}>
{value.worthIt
? '✅ Great value for your points!'
: '⚠️ Consider saving points for better offers'}
</div>
</div>
);
};Offer Comparison Tool
const OfferComparison = ({ offers }) => {
const [sortBy, setSortBy] = useState('pointsRequired');
const [showOnlyAvailable, setShowOnlyAvailable] = useState(true);
const filteredOffers = offers.filter(offer => {
if (!showOnlyAvailable) return true;
// Check if offer has any blocking restrictions
const hasBlockingRestrictions = offer.restrictions.some(restriction =>
restriction.type === 'requires_stay' && !hasActiveStay()
);
return !hasBlockingRestrictions;
});
const sortedOffers = [...filteredOffers].sort((a, b) => {
switch (sortBy) {
case 'pointsRequired':
return a.pointsRequired - b.pointsRequired;
case 'retailValue':
return (b.retailValue || 0) - (a.retailValue || 0);
case 'value':
return calculateOfferValue(b) - calculateOfferValue(a);
case 'expiration':
const aExpiry = a.validUntil ? new Date(a.validUntil) : new Date('2099-12-31');
const bExpiry = b.validUntil ? new Date(b.validUntil) : new Date('2099-12-31');
return aExpiry - bExpiry;
default:
return 0;
}
});
const calculateOfferValue = (offer) => {
const pointsValue = offer.pointsRequired * 0.01; // Assume 1 cent per point
const retailValue = offer.retailValue || 0;
return Math.max(0, retailValue - pointsValue);
};
return (
<div className="offer-comparison">
<div className="comparison-controls">
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="pointsRequired">Sort by Points Required</option>
<option value="retailValue">Sort by Retail Value</option>
<option value="value">Sort by Best Value</option>
<option value="expiration">Sort by Expiration</option>
</select>
<label>
<input
type="checkbox"
checked={showOnlyAvailable}
onChange={(e) => setShowOnlyAvailable(e.target.checked)}
/>
Show only available offers
</label>
</div>
<div className="comparison-table">
<div className="table-header">
<div>Offer</div>
<div>Points</div>
<div>Retail Value</div>
<div>Value Score</div>
<div>Expires</div>
<div>Status</div>
</div>
{sortedOffers.map(offer => {
const value = calculateOfferValue(offer);
const isExpiringSoon = offer.validUntil &&
new Date(offer.validUntil) - new Date() < 7 * 24 * 60 * 60 * 1000;
return (
<div key={offer.offerId} className="table-row">
<div className="offer-info">
<div className="offer-name">{offer.name}</div>
<div className="offer-category">{offer.category?.name}</div>
</div>
<div className="points-column">
{offer.pointsRequired.toLocaleString()}
</div>
<div className="value-column">
${offer.retailValue?.toFixed(2) || 'N/A'}
</div>
<div className={`value-score ${value > 0 ? 'positive' : 'neutral'}`}>
{value > 0 ? `+$${value.toFixed(2)}` : '$0.00'}
</div>
<div className="expiry-column">
{offer.validUntil ? (
<span className={isExpiringSoon ? 'expiring-soon' : ''}>
{new Date(offer.validUntil).toLocaleDateString()}
</span>
) : (
'No expiration'
)}
</div>
<div className="status-column">
{offer.requiresStay ? (
<span className="requires-stay">Requires Stay</span>
) : (
<span className="available">Available</span>
)}
</div>
</div>
);
})}
</div>
</div>
);
};Offer Categories
Common Categories
- Food & Beverage: Restaurant credits, complimentary meals, wine tastings
- Room & Accommodations: Upgrades, late checkout, early check-in
- Wellness & Spa: Treatment discounts, access passes, wellness packages
- Recreation & Activities: Tours, equipment rentals, facility access
Redemption Types
- dining: Restaurant and bar offers
- spa: Wellness and spa services
- room_upgrade: Accommodation enhancements
- amenity_access: Facility and service access
- experience: Tours and activities
- retail: Shopping discounts and credits
Offer Restrictions
Restriction Types
const restrictionTypes = {
requires_stay: {
description: 'Must have an active hotel reservation',
appliesToGuests: true,
blockingForNonGuests: true
},
check_in_window: {
description: 'Only available during specified check-in period',
appliesToGuests: true,
timeRestricted: true
}
};
const interpretRestrictions = (offer) => {
const interpretation = {
canRedeem: true,
warnings: [],
blockers: []
};
offer.restrictions.forEach(restriction => {
switch (restriction.type) {
case 'requires_stay':
if (!hasActiveStay()) {
interpretation.canRedeem = false;
interpretation.blockers.push('Active hotel stay required');
}
break;
case 'check_in_window':
if (offer.disableBeforeCheckInHours) {
const hoursUntilCheckIn = getHoursUntilCheckIn();
if (hoursUntilCheckIn > offer.disableBeforeCheckInHours) {
interpretation.warnings.push(
`Available ${offer.disableBeforeCheckInHours} hours before check-in`
);
}
}
break;
}
});
return interpretation;
};Related Endpoints
- Member Offers - Authenticated member offers with personalization
- Claim Offer - Redeem offers (requires authentication)
- Property Details - Property information for context