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
Promise-Based (Recommended)
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:
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
Modal Displays
A responsive modal overlay appears showing:
Offer details and rewards
Instructions
Call-to-action buttons
Multiple offers in a carousel (if applicable)
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
Provisional Grant
When user claims an offer:
SDK sends provisional grant signal to API
Entitlements are refreshed automatically
Success screen is shown
Modal closes after a few seconds
Verification
The advertiser confirms the conversion:
User completes offer on advertiser site
Advertiser sends postback to Encore
Provisional entitlement becomes verified
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:
Node.js / Express
Python / Flask
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: