# Get Public Offers
> Source: /api-reference/public/offers/get-offers
> Get available loyalty offers without authentication
> Endpoint: GET /public/offers

## Overview [#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 [#authentication]

No authentication required. This is a public endpoint.

## Query Parameters [#query-parameters]

<ParamField query="propertyId" type="string">
  Filter offers by property document ID
</ParamField>

<ParamField query="categoryId" type="number">
  Filter offers by category ID
</ParamField>

<ParamField query="redemptionType" type="string">
  Filter by redemption type (e.g., "dining", "spa", "room_upgrade")
</ParamField>

<ParamField query="limit" type="number" default="20">
  Maximum number of offers to return (1-100)
</ParamField>

<ParamField query="offset" type="number" default="0">
  Number of offers to skip for pagination
</ParamField>

<RequestExample>

```bash cURL - All Public Offers
curl -X GET "https://api.journey.com/v1/public/offers?limit=10&offset=0"
```

```bash cURL - Property-Specific Offers
curl -X GET "https://api.journey.com/v1/public/offers?propertyId=abc123def456ghi789jkl012&limit=5"
```

```bash cURL - Category Filter
curl -X GET "https://api.journey.com/v1/public/offers?categoryId=1&limit=20"
```

```typescript TypeScript
const getPublicOffers = async (filters = {}) => {
  const params = new URLSearchParams();
  
  if (filters.propertyId) params.append('propertyId', filters.propertyId);
  if (filters.categoryId) params.append('categoryId', filters.categoryId.toString());
  if (filters.redemptionType) params.append('redemptionType', filters.redemptionType);
  if (filters.limit) params.append('limit', filters.limit.toString());
  if (filters.offset) params.append('offset', filters.offset.toString());
  
  const url = `https://api.journey.com/v1/public/offers${params.toString() ? `?${params}` : ''}`;
  
  const response = await fetch(url, { method: 'GET' });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
};
```

```javascript JavaScript
const getPublicOffers = async (filters = {}) => {
  const params = new URLSearchParams();
  
  if (filters.propertyId) params.append('propertyId', filters.propertyId);
  if (filters.categoryId) params.append('categoryId', filters.categoryId.toString());
  if (filters.redemptionType) params.append('redemptionType', filters.redemptionType);
  if (filters.limit) params.append('limit', filters.limit.toString());
  if (filters.offset) params.append('offset', filters.offset.toString());
  
  const url = `https://api.journey.com/v1/public/offers${params.toString() ? `?${params}` : ''}`;
  
  const response = await fetch(url, { method: 'GET' });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
};
```

</RequestExample>

## Response [#response]

<ResponseField name="items" type="array" required>
  Array of available offers
  
  <Expandable title="offer items">
    <ResponseField name="offerId" type="string" required>
      Unique offer identifier
    </ResponseField>
    
    <ResponseField name="name" type="string" required>
      Offer name/title
    </ResponseField>
    
    <ResponseField name="shortDescription" type="string">
      Brief description for preview/cards
    </ResponseField>
    
    <ResponseField name="description" type="string">
      Full offer description
    </ResponseField>
    
    <ResponseField name="pointsRequired" type="number" required>
      Points required to redeem this offer
    </ResponseField>
    
    <ResponseField name="retailValue" type="number">
      Retail value of the offer in USD
    </ResponseField>
    
    <ResponseField name="discountAmount" type="number">
      Discount amount in USD (if applicable)
    </ResponseField>
    
    <ResponseField name="discountType" type="string">
      Type of discount (percentage, fixed_amount, etc.)
    </ResponseField>
    
    <ResponseField name="redemptionType" type="string" required>
      How the offer is redeemed (dining, spa, room_upgrade, etc.)
    </ResponseField>
    
    <ResponseField name="category" type="object">
      Offer category information
      
      <Expandable title="category object">
        <ResponseField name="id" type="number" required>
          Category ID
        </ResponseField>
        
        <ResponseField name="name" type="string" required>
          Category name
        </ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="heroImageUrl" type="string">
      Main image URL for the offer
    </ResponseField>
    
    <ResponseField name="cardImageUrl" type="string">
      Card/thumbnail image URL
    </ResponseField>
    
    <ResponseField name="howToRedeem" type="string">
      Instructions for redeeming the offer
    </ResponseField>
    
    <ResponseField name="termsAndConditions" type="string">
      Offer terms and conditions
    </ResponseField>
    
    <ResponseField name="rules" type="string">
      Additional rules and restrictions
    </ResponseField>
    
    <ResponseField name="isEligible" type="boolean" required>
      Whether this offer is generally eligible (always true for public offers)
    </ResponseField>
    
    <ResponseField name="restrictions" type="array" required>
      Active restrictions that may apply
      
      <Expandable title="restriction items">
        <ResponseField name="type" type="string" required>
          Restriction type
          <br />Options: `requires_stay`, `check_in_window`
        </ResponseField>
        
        <ResponseField name="message" type="string" required>
          Human-readable restriction message
        </ResponseField>
      </Expandable>
    </ResponseField>
    
    <ResponseField name="requiresStay" type="boolean" required>
      Whether offer requires an active hotel stay to redeem
    </ResponseField>
    
    <ResponseField name="disableBeforeCheckInHours" type="number">
      Hours before check-in when offer becomes unavailable (null if no restriction)
    </ResponseField>
    
    <ResponseField name="validUntil" type="string">
      Offer expiration date (ISO timestamp, null if no expiration)
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="total" type="number" required>
  Total number of offers matching the criteria
</ResponseField>

<ResponseField name="offset" type="number">
  Current pagination offset
</ResponseField>

<ResponseField name="limit" type="number">
  Current pagination limit
</ResponseField>

<ResponseExample>

```json 200 Response - Mixed Public Offers
{
  "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
}
```

```json 200 Response - Property-Specific Offers
{
  "items": [
    {
      "offerId": "offer_beachclub_access_miami",
      "name": "Exclusive Beach Club Access",
      "shortDescription": "Full day access to private beach club",
      "description": "Enjoy exclusive access to our private beach club with premium loungers, cabanas, and beachside service.",
      "pointsRequired": 15000,
      "retailValue": 150.00,
      "discountAmount": null,
      "discountType": null,
      "redemptionType": "amenity_access",
      "category": {
        "id": 4,
        "name": "Recreation & Activities"
      },
      "heroImageUrl": "https://cdn.journey.com/offers/beach-club-hero.jpg",
      "cardImageUrl": "https://cdn.journey.com/offers/beach-club-card.jpg",
      "howToRedeem": "Visit the concierge desk with confirmation",
      "termsAndConditions": "Valid for registered guests only. Access for up to 4 people. Food and beverages sold separately.",
      "rules": "Advance reservation required. Weather dependent. May be closed for private events.",
      "isEligible": true,
      "restrictions": [
        {
          "type": "requires_stay",
          "message": "Must be staying at the property"
        }
      ],
      "requiresStay": true,
      "disableBeforeCheckInHours": 48,
      "validUntil": "2024-09-30T23:59:59Z"
    }
  ],
  "total": 1,
  "offset": 0,
  "limit": 5
}
```

```json 400 Error - Invalid Parameters
{
  "statusCode": 400,
  "message": [
    "limit must not be greater than 100",
    "offset must be a positive number"
  ],
  "error": "Bad Request"
}
```

</ResponseExample>

## Use Cases [#use-cases]

### Public Offers Showcase [#public-offers-showcase]
```typescript
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 [#offer-value-calculator]
```typescript
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 [#offer-comparison-tool]
```typescript
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 [#offer-categories]

### Common 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 [#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 [#offer-restrictions]

### Restriction Types [#restriction-types]
```typescript
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 [#related-endpoints]

- [Member Offers](/api-reference/members/loyalty/get-offers) - Authenticated member offers with personalization
- [Claim Offer](/api-reference/members/loyalty/claim-offer) - Redeem offers (requires authentication)
- [Property Details](/api-reference/public/properties/get-property-details) - Property information for context
