Journey Docs
Public APIOffers

Get Public Offers

Get available loyalty offers without authentication

GET/public/offers

Overview

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

propertyIdquerystring

Filter offers by property document ID

categoryIdquerynumber

Filter offers by category ID

redemptionTypequerystring

Filter by redemption type (e.g., "dining", "spa", "room_upgrade")

limitquerynumberdefault: 20

Maximum number of offers to return (1-100)

offsetquerynumberdefault: 0

Number of offers to skip for pagination

Request

curl -X GET "https://api.journey.com/v1/public/offers?limit=10&offset=0"

Response

itemsarrayrequired

Array of available offers

offer items
offerIdstringrequired

Unique offer identifier

namestringrequired

Offer name/title

shortDescriptionstring

Brief description for preview/cards

descriptionstring

Full offer description

pointsRequirednumberrequired

Points required to redeem this offer

retailValuenumber

Retail value of the offer in USD

discountAmountnumber

Discount amount in USD (if applicable)

discountTypestring

Type of discount (percentage, fixed_amount, etc.)

redemptionTypestringrequired

How the offer is redeemed (dining, spa, room_upgrade, etc.)

categoryobject

Offer category information

category object
idnumberrequired

Category ID

namestringrequired

Category name

heroImageUrlstring

Main image URL for the offer

cardImageUrlstring

Card/thumbnail image URL

howToRedeemstring

Instructions for redeeming the offer

termsAndConditionsstring

Offer terms and conditions

rulesstring

Additional rules and restrictions

isEligiblebooleanrequired

Whether this offer is generally eligible (always true for public offers)

restrictionsarrayrequired

Active restrictions that may apply

restriction items
typestringrequired

Restriction type
Options: requires_stay, check_in_window

messagestringrequired

Human-readable restriction message

requiresStaybooleanrequired

Whether offer requires an active hotel stay to redeem

disableBeforeCheckInHoursnumber

Hours before check-in when offer becomes unavailable (null if no restriction)

validUntilstring

Offer expiration date (ISO timestamp, null if no expiration)

totalnumberrequired

Total number of offers matching the criteria

offsetnumber

Current pagination offset

limitnumber

Current 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;
};

On this page