Journey Docs
Members APIDiscover

Claim Bonus

Claim a one-time easter egg bonus

POST/members/me/features/discover/bonuses/{code}/claim

Overview

Claim a one-time easter egg bonus. Easter eggs are special promotional bonuses that provide gift points to members. Each bonus can only be claimed once per member and awards non-tier-eligible gift points.

The system automatically validates:

  • Bonus code exists and is active
  • Member hasn't already claimed this bonus
  • Awards appropriate points via the ledger system

Authentication

Requires a valid Clerk JWT token in the Authorization header.

Path Parameters

codepathstringrequired

The easter egg bonus code to claim

Request

curl -X POST "http://localhost:3000/v1/members/me/features/discover/bonuses/WELCOME2024/claim" \
  -H "Authorization: Bearer YOUR_CLERK_JWT"

Response

Returns the details of the claimed bonus including points awarded and timestamp.

eggCodestringrequired

The bonus code that was claimed

displayNamestringrequired

Human-readable name of the bonus

pointsAwardednumberrequired

Number of gift points awarded to the member

claimedAtstringrequired

ISO 8601 timestamp when the bonus was claimed

Response

{
  "eggCode": "WELCOME2024",
  "displayName": "Welcome to Journey 2024",
  "pointsAwarded": 5000,
  "claimedAt": "2024-04-20T15:30:45Z"
}

Gift Points vs Earning Points

Gift Points Characteristics

  • Non-tier-eligible: Don't count toward tier progression
  • Immediate availability: Added to wallet immediately
  • Promotional source: Marked with giftSource: 'promotion'
  • One-time only: Each easter egg can only be claimed once per member

Ledger Transaction

// The service creates a gift transaction with metadata
const giftResult = await ledgerFacade.giftPoints({
  memberId,
  amount: egg.pointValue,
  reason: `Easter egg bonus: ${egg.friendlyName}`,
  giftSource: 'promotion',
  metadata: {
    easterEggCode: egg.code,
    easterEggId: egg.id,
    easterEggDisplayName: egg.displayName,
  },
});

Use Cases

Claim Flow with Validation

const handleBonusClaim = async (code: string) => {
  try {
    // Check if already claimed
    const status = await getBonusStatus(code);
    if (status.claimed) {
      return { success: false, message: 'Bonus already claimed' };
    }
    
    // Attempt claim
    const result = await claimBonus(code);
    
    // Show success message
    showSuccessToast(`Claimed ${result.pointsAwarded.toLocaleString()} points!`);
    
    // Refresh wallet balance
    refreshWalletBalance();
    
    return { success: true, result };
  } catch (error) {
    if (error.status === 409) {
      return { success: false, message: 'Bonus already claimed' };
    }
    
    if (error.status === 404) {
      return { success: false, message: 'Invalid bonus code' };
    }
    
    return { success: false, message: 'Failed to claim bonus' };
  }
};

Bonus Discovery UI

const BonusClaimCard = ({ bonus }) => {
  const [claiming, setClaiming] = useState(false);
  const [claimed, setClaimed] = useState(false);
  const [claimResult, setClaimResult] = useState(null);
  
  const handleClaim = async () => {
    setClaiming(true);
    
    try {
      const result = await claimBonus(bonus.code);
      setClaimResult(result);
      setClaimed(true);
      
      // Animate points addition
      animatePointsIncrease(result.pointsAwarded);
      
    } catch (error) {
      showError('Failed to claim bonus');
    } finally {
      setClaiming(false);
    }
  };
  
  return (
    <div className="bonus-card">
      <h3>{bonus.displayName}</h3>
      <div className="points-value">
        {bonus.pointValue.toLocaleString()} Points
      </div>
      
      {claimed ? (
        <div className="claimed-status">
          <CheckIcon />
          <span>Claimed {claimResult?.claimedAt && formatDate(claimResult.claimedAt)}</span>
        </div>
      ) : (
        <button 
          onClick={handleClaim}
          disabled={claiming}
          className="claim-button"
        >
          {claiming ? <Spinner /> : 'Claim Bonus'}
        </button>
      )}
    </div>
  );
};

Wallet Integration

const trackBonusClaim = async (claimResult: ClaimEasterEggResult) => {
  // Track analytics event
  analytics.track('Bonus Claimed', {
    bonusCode: claimResult.eggCode,
    pointsAwarded: claimResult.pointsAwarded,
    claimedAt: claimResult.claimedAt
  });
  
  // Update local wallet state
  updateWalletBalance(currentBalance + claimResult.pointsAwarded);
  
  // Add transaction to activity feed
  addTransaction({
    type: 'gift',
    amount: claimResult.pointsAwarded,
    description: `Easter egg bonus: ${claimResult.displayName}`,
    createdAt: claimResult.claimedAt
  });
};

Error Handling

Conflict Detection

The service automatically prevents double-claiming by checking for existing claims before processing. If a member attempts to claim the same bonus twice, they receive a 409 Conflict error.

Validation Flow

const claimWithRetry = async (code: string, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await claimBonus(code);
    } catch (error) {
      if (error.status === 409 || error.status === 404) {
        // Don't retry for conflict or not found
        throw error;
      }
      
      if (attempt === maxRetries) {
        throw new Error('Failed to claim bonus after retries');
      }
      
      // Wait before retry
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
};

Ledger Integration

Transaction Creation

When a bonus is claimed, the system:

  1. Validates the bonus code and member eligibility
  2. Creates a gift points transaction in the ledger
  3. Records the claim in the easter eggs system
  4. Returns claim details to the client

Metadata Tracking

// Transaction metadata includes:
{
  easterEggCode: 'WELCOME2024',
  easterEggId: 123,
  easterEggDisplayName: 'Welcome to Journey 2024'
}

This metadata allows for easy tracking and reporting of bonus claims across the platform.

On this page