Skip to main content
Present Encore retention offers to users at key moments in your application.

Overview

Encore allows you to present targeted offers to users in exchange for rewards (free trials, discounts, credits) at critical moments like cancellation flows, feature paywalls, or onboarding. The SDK provides multiple presentation methods to fit your application’s architecture.

Basic Presentation

The simplest way to present an offer:
import Encore from '@encore/web-sdk';

const result = await Encore.presentOffer();

if (result.granted) {
  console.log('Entitlement granted:', result.entitlement);
  // Unlock premium feature
} else {
  console.log('Not granted:', result.reason);
  // Handle decline
}
This opens a modal overlay with available offers. The user can:
  • Claim an offer - Opens the advertiser URL in a new tab
  • Decline the offer - Shows the next offer or closes the modal
  • Close the modal - Dismisses without claiming

Presentation Methods

Use await for simple, linear flow control:
async function showPremiumFeature() {
  // Check access via backend
  const hasAccess = await checkServerSideAccess(userId);
  if (hasAccess) {
    showContent();
    return;
  }
  
  // Present offer
  const result = await Encore.presentOffer();
  
  if (result.granted) {
    // Notify backend and validate
    await notifyBackend(userId, result.entitlement);
    showContent();
  } else {
    // User declined
    showUpgradePrompt();
  }
}

async function checkServerSideAccess(userId) {
  const response = await fetch('/api/validate-entitlements', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId }),
  });
  const entitlements = await response.json();
  return !!entitlements.all?.freeTrial;
}

async function notifyBackend(userId, entitlement) {
  await fetch('/api/offer-granted', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId, entitlement }),
  });
}

Callback-Based

Use callbacks for more complex flow control:
await Encore.presentOffer({
  onGranted: async (entitlement) => {
    console.log('User granted:', entitlement);
    
    // Notify backend of the grant
    await fetch('/api/offer-granted', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        userId: getCurrentUserId(),
        entitlement 
      }),
    });
    
    unlockFeature();
    trackEvent('offer_accepted');
  },
  onNotGranted: (reason) => {
    console.log('User declined:', reason);
    if (reason === 'noOffersAvailable') {
      showAlternativeFlow();
    }
    trackEvent('offer_declined', { reason });
  },
  onError: (error) => {
    console.error('Error presenting offer:', error);
    showErrorMessage();
  }
});

Fluent Builder Pattern

For expressive, chainable code:
const result = await Encore.placement()
  .onGranted(async (entitlement) => {
    // Notify backend
    await fetch('/api/offer-granted', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        userId: getCurrentUserId(),
        entitlement 
      }),
    });
    unlockFeature();
  })
  .onNotGranted((reason) => {
    handleDecline(reason);
  })
  .show();

Framework Integration

  • React
  • Vue
  • Angular
  • Svelte
  • Vanilla JS
import { useState, useEffect } from 'react';
import Encore from '@encore/web-sdk';

function PremiumFeature() {
  const [hasAccess, setHasAccess] = useState(false);
  const [loading, setLoading] = useState(true);
  const userId = getCurrentUserId();
  
  useEffect(() => {
    checkAccess();
  }, []);
  
  const checkAccess = async () => {
    try {
      const response = await fetch('/api/validate-entitlements', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId }),
      });
      const entitlements = await response.json();
      setHasAccess(!!entitlements.all?.freeTrial);
    } finally {
      setLoading(false);
    }
  };
  
  const handleUnlock = async () => {
    setLoading(true);
    try {
      const result = await Encore.presentOffer();
      if (result.granted) {
        // Notify backend
        await fetch('/api/offer-granted', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId, entitlement: result.entitlement }),
        });
        
        setHasAccess(true);
      }
    } finally {
      setLoading(false);
    }
  };
  
  if (loading && !hasAccess) {
    return <LoadingSpinner />;
  }
  
  if (hasAccess) {
    return <PremiumContent />;
  }
  
  return (
    <div>
      <h2>Premium Feature</h2>
      <p>Unlock this feature with a quick offer</p>
      <button onClick={handleUnlock} disabled={loading}>
        {loading ? 'Loading...' : 'Unlock Now'}
      </button>
    </div>
  );
}

Handling Results

Presentation Result

The presentOffer() method returns a result object:
interface PresentationResult {
  granted: boolean
  entitlement?: EntitlementType
  reason?: NotGrantedReason
}
Example:
const result = await Encore.presentOffer();

if (result.granted) {
  console.log('Entitlement type:', result.entitlement.type);
  // { type: 'freeTrial', value: 30, unit: 'days' }
} else {
  console.log('Reason:', result.reason);
  // 'userClosedModal' | 'noOffersAvailable' | etc.
}

Not Granted Reasons

When granted is false, the reason indicates why:
const result = await Encore.presentOffer();

if (!result.granted) {
  switch (result.reason) {
    case 'userClosedModal':
      console.log('User clicked X or pressed ESC');
      break;
    case 'userClickedOutside':
      console.log('User clicked outside the modal');
      break;
    case 'userDeclinedLastOffer':
      console.log('User clicked "No Thanks" on the last offer');
      break;
    case 'noOffersAvailable':
      console.log('No offers available for this user');
      showAlternativeFlow();
      break;
    default:
      if (typeof result.reason === 'object' && result.reason.type === 'error') {
        console.error('Error occurred:', result.reason.error);
      }
  }
}

Common Use Cases

Cancellation Flow

Prevent subscription cancellations with targeted offers:
async function handleCancelSubscription() {
  // Show confirmation
  const confirmed = await showCancelConfirmation();
  if (!confirmed) return;
  
  // Present retention offer
  const result = await Encore.presentOffer();
  
  if (result.granted) {
    // User accepted offer - cancel the cancellation
    showMessage('Great! Your subscription continues with a special offer.');
    cancelCancellationFlow();
  } else if (result.reason !== 'noOffersAvailable') {
    // User declined - proceed with cancellation
    await cancelSubscription();
    showMessage('Your subscription has been cancelled.');
  }
}

Feature Paywall

Gate premium features behind offers:
async function showPremiumFeature() {
  // Check access via backend
  const response = await fetch('/api/validate-entitlements', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId: getCurrentUserId() }),
  });
  const entitlements = await response.json();
  
  if (entitlements.all?.freeTrial) {
    renderPremiumContent();
    return;
  }
  
  // Show paywall with offer option
  renderPaywall({
    onUnlock: async () => {
      const result = await Encore.presentOffer();
      if (result.granted) {
        // Notify backend
        await fetch('/api/offer-granted', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ 
            userId: getCurrentUserId(),
            entitlement: result.entitlement 
          }),
        });
        
        renderPremiumContent();
      } else {
        redirectToPricingPage();
      }
    }
  });
}

Onboarding Enhancement

Enhance onboarding with premium access offers:
async function completeOnboarding() {
  // Complete standard onboarding
  await finishOnboardingSteps();
  
  // Present welcome offer
  const result = await Encore.presentOffer();
  
  if (result.granted) {
    // Enhanced onboarding with premium features
    showPremiumOnboarding();
  } else {
    // Standard onboarding flow
    showStandardOnboarding();
  }
}

Re-engagement Campaign

Win back inactive users:
async function checkInactiveUser() {
  const lastActive = getUserLastActiveDate();
  const daysSinceActive = (Date.now() - lastActive) / (1000 * 60 * 60 * 24);
  
  if (daysSinceActive > 30) {
    // Set user segment
    Encore.setUserAttributes({
      lastActiveDate: new Date(lastActive).toISOString(),
      custom: { userSegment: 'inactive' }
    });
    
    // Present re-engagement offer
    const result = await Encore.presentOffer();
    if (result.granted) {
      trackReengagementSuccess();
    }
  }
}

Offer Presentation Flow

Understanding the complete offer flow:
1

SDK Fetches Offers

The SDK calls the Encore API to fetch available offers based on:
  • User ID and attributes
  • User location
  • Previous offer history
  • Offer targeting rules
2

Modal Displays

A responsive modal overlay appears showing:
  • Offer details and rewards
  • Instructions
  • Call-to-action buttons
  • Multiple offers in a carousel (if applicable)
3

User Interacts

The user can:
  • Claim offer - Opens advertiser URL in a new tab
  • Decline offer - Shows next offer or closes
  • Close modal - Dismisses without action
4

Provisional Grant

When user claims an offer:
  1. SDK sends provisional grant signal to API
  2. Entitlements are refreshed automatically
  3. Success screen is shown
  4. Modal closes after a few seconds
5

Verification

The advertiser confirms the conversion:
  1. User completes offer on advertiser site
  2. Advertiser sends postback to Encore
  3. Provisional entitlement becomes verified
  4. Your app can check verification status
The SDK automatically refreshes entitlements after an offer is claimed. You don’t need to manually call refreshEntitlements().

Best Practices

1. Check Before Presenting

Always check if user already has access:
// Good
if (!Encore.isActive({ type: 'freeTrial' })) {
  await Encore.presentOffer();
}

// Avoid showing offers to users who already have access

2. Handle All Outcomes

Prepare for all possible results:
const result = await Encore.presentOffer();

if (result.granted) {
  // Success path
} else if (result.reason === 'noOffersAvailable') {
  // No offers - show alternative
} else {
  // User declined - respect their choice
}

3. Provide Context

Explain why you’re showing an offer:
// Good: Clear context
<button onClick={() => Encore.presentOffer()}>
  Get Free Premium Access
</button>

// Better: Specific benefit
<button onClick={() => Encore.presentOffer()}>
  Unlock 30-Day Free Trial
</button>

4. Don’t Be Intrusive

Show offers at natural decision points:
// Good: User initiated
button.addEventListener('click', () => Encore.presentOffer());

// Avoid: Immediate interruption
window.addEventListener('load', () => Encore.presentOffer());

5. Track Analytics

Monitor offer performance:
const result = await Encore.presentOffer();

if (result.granted) {
  // Notify backend
  await fetch('/api/offer-granted', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ 
      userId: getCurrentUserId(),
      entitlement: result.entitlement 
    }),
  });
  
  analytics.track('offer_accepted', {
    entitlementType: result.entitlement.type
  });
} else {
  analytics.track('offer_declined', {
    reason: result.reason
  });
}

Backend Validation After Grants

When a user claims an offer, always notify your backend to validate and log the grant.

Backend Endpoint

Create an endpoint to handle offer grants:
app.post('/api/offer-granted', async (req, res) => {
  try {
    const { userId, entitlement } = req.body;
    
    // Log the grant
    await logOfferGrant(userId, entitlement);
    
    // Validate current entitlements with Encore
    const method = 'POST';
    const path = '/encore/v1/entitlements/server';
    const body = { user_id: userId };
    
    const signature = generateHmacSignature(
      process.env.ENCORE_PRIVATE_API_KEY,
      method,
      path,
      body
    );
    
    const timestamp = Math.floor(Date.now() / 1000);
    
    const response = await fetch(`${process.env.ENCORE_BASE_URL}${path}`, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.ENCORE_PUBLIC_API_KEY,
        'X-Signature': signature,
        'X-Timestamp': timestamp.toString(),
      },
      body: JSON.stringify(body),
    });
    
    const entitlements = await response.json();
    
    // Update user's access
    await updateUserAccess(userId, entitlements);
    
    res.json({ success: true, entitlements });
  } catch (error) {
    console.error('Failed to process grant:', error);
    res.status(500).json({ error: 'Failed to process grant' });
  }
});

Frontend Integration

Call your backend after an offer is granted:
const result = await Encore.presentOffer();

if (result.granted) {
  // Notify backend immediately
  const response = await fetch('/api/offer-granted', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      userId: getCurrentUserId(),
      entitlement: result.entitlement,
    }),
  });
  
  const { entitlements } = await response.json();
  
  // Update UI based on validated entitlements
  if (entitlements.all?.freeTrial) {
    unlockPremiumFeatures();
  }
}
Always validate entitlements on your server before granting access to premium features or processing payments. Client-side validation can be bypassed.

Next Steps

Now that you understand offer presentation, learn how to: For advanced patterns and framework-specific examples, see: