Skip to main content
Re-validate user entitlements on your backend to check for updated states, such as when provisional entitlements become verified.
The Web SDK’s client-side refreshEntitlements() method has been deprecated. Entitlement validation must be performed server-side using HMAC-authenticated API requests.

When to Refresh

Refresh entitlements in these scenarios:
1

After Offer Claimed

When a user claims an offer through the SDK, immediately validate their new entitlements.
const result = await Encore.presentOffer();
if (result.granted) {
  // Refresh entitlements from server
  const entitlements = await validateEntitlements(userId);
  updateUI(entitlements);
}
2

Checking Verification Status

After a provisional grant, periodically check if it’s been verified by the advertiser.
// Poll for verification
const isVerified = await checkUntilVerified(userId, transactionId);
3

User Returns to App

When a user returns from the advertiser site or navigates back to your app.
window.addEventListener('focus', async () => {
  const entitlements = await validateEntitlements(userId);
  updateUI(entitlements);
});
4

Session Start

When a user starts a new session or logs in.
app.post('/auth/login', async (req, res) => {
  const user = await authenticateUser(req.body);
  const entitlements = await validateEntitlements(user.id);
  req.session.entitlements = entitlements;
  res.json({ user, entitlements });
});

Basic Refresh Pattern

Backend Validation Function

Create a reusable function to validate entitlements:
import crypto from 'crypto';

function generateHmacSignature(privateKey, method, path, body) {
  const bodyJson = JSON.stringify(body);
  const canonical = `${method.toUpperCase()}|${path}|${bodyJson}`;
  const hmac = crypto.createHmac('sha256', privateKey);
  hmac.update(canonical);
  return hmac.digest('hex');
}

async function validateEntitlements(userId) {
  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),
  });
  
  if (!response.ok) {
    throw new Error(`Encore API error: ${response.statusText}`);
  }
  
  return await response.json();
}

// Refresh endpoint
app.post('/api/refresh-entitlements', async (req, res) => {
  try {
    const { userId } = req.body;
    const entitlements = await validateEntitlements(userId);
    res.json(entitlements);
  } catch (error) {
    console.error('Refresh failed:', error);
    res.status(500).json({ error: 'Failed to refresh entitlements' });
  }
});

Frontend Refresh

Call your backend’s refresh endpoint:
import { useState } from 'react';

function useRefreshEntitlements() {
  const [loading, setLoading] = useState(false);
  
  const refresh = async (userId) => {
    setLoading(true);
    try {
      const response = await fetch('/api/refresh-entitlements', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId }),
      });
      
      if (!response.ok) {
        throw new Error('Refresh failed');
      }
      
      return await response.json();
    } catch (error) {
      console.error('Failed to refresh:', error);
      throw error;
    } finally {
      setLoading(false);
    }
  };
  
  return { refresh, loading };
}

// Usage
function MyComponent() {
  const { refresh, loading } = useRefreshEntitlements();
  const [entitlements, setEntitlements] = useState(null);
  
  const handleRefresh = async () => {
    const data = await refresh('user_123');
    setEntitlements(data);
  };
  
  return (
    <button onClick={handleRefresh} disabled={loading}>
      {loading ? 'Refreshing...' : 'Refresh Entitlements'}
    </button>
  );
}

Verification Polling

Check periodically if a provisional entitlement becomes verified:

Backend Polling Function

async function pollForVerification(userId, transactionId, options = {}) {
  const maxAttempts = options.maxAttempts || 12; // Default: 12 attempts
  const delayMs = options.delayMs || 5000;       // Default: 5 seconds
  const onProgress = options.onProgress || (() => {});
  
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    // Get fresh entitlements
    const entitlements = await validateEntitlements(userId);
    
    // Check if entitlement is now verified
    const verified = Object.values(entitlements.verified).find(
      ent => ent.transactionId === transactionId
    );
    
    if (verified) {
      onProgress({ status: 'verified', attempt, entitlement: verified });
      return { status: 'verified', entitlement: verified };
    }
    
    // Check if still provisional
    const provisional = Object.values(entitlements.provisional).find(
      ent => ent.transactionId === transactionId
    );
    
    if (!provisional) {
      // Entitlement disappeared (expired or revoked)
      onProgress({ status: 'failed', attempt });
      return { status: 'failed' };
    }
    
    onProgress({ status: 'pending', attempt, entitlement: provisional });
    
    // Wait before next attempt
    if (attempt < maxAttempts - 1) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }
  
  // Timeout - still provisional
  return { status: 'timeout' };
}

// Endpoint
app.post('/api/poll-verification', async (req, res) => {
  const { userId, transactionId } = req.body;
  
  try {
    const result = await pollForVerification(userId, transactionId, {
      maxAttempts: 12,
      delayMs: 5000,
    });
    
    res.json(result);
  } catch (error) {
    console.error('Polling failed:', error);
    res.status(500).json({ error: 'Polling failed' });
  }
});

Frontend Usage

async function waitForVerification(userId, transactionId) {
  try {
    const response = await fetch('/api/poll-verification', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId, transactionId }),
    });
    
    const result = await response.json();
    
    if (result.status === 'verified') {
      console.log('Entitlement verified!');
      showVerifiedBadge();
      return true;
    } else if (result.status === 'timeout') {
      console.log('Verification still pending');
      showPendingMessage();
      return false;
    } else {
      console.log('Verification failed');
      showErrorMessage();
      return false;
    }
  } catch (error) {
    console.error('Polling error:', error);
    return false;
  }
}

// Use after offer claimed
const result = await Encore.presentOffer();
if (result.granted && result.entitlement) {
  // Show provisional access immediately
  unlockFeature();
  
  // Wait for verification (non-blocking)
  waitForVerification(userId, result.entitlement.transactionId);
}

Caching Strategies

Time-Based Cache

Cache validation results for a fixed duration:
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getCachedEntitlements(userId) {
  const now = Date.now();
  const cached = cache.get(userId);
  
  // Return cached if still valid
  if (cached && now - cached.timestamp < CACHE_TTL) {
    console.log('Returning cached entitlements');
    return cached.data;
  }
  
  // Fetch fresh data
  console.log('Fetching fresh entitlements');
  const entitlements = await validateEntitlements(userId);
  
  // Update cache
  cache.set(userId, {
    data: entitlements,
    timestamp: now,
  });
  
  return entitlements;
}

// Endpoint with caching
app.post('/api/validate-entitlements', async (req, res) => {
  try {
    const { userId, forceRefresh } = req.body;
    
    let entitlements;
    if (forceRefresh) {
      // Bypass cache
      entitlements = await validateEntitlements(userId);
      cache.set(userId, { data: entitlements, timestamp: Date.now() });
    } else {
      // Use cache if available
      entitlements = await getCachedEntitlements(userId);
    }
    
    res.json(entitlements);
  } catch (error) {
    res.status(500).json({ error: 'Validation failed' });
  }
});

Session-Based Cache

Store entitlements in user session:
import session from 'express-session';

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 30 * 60 * 1000 }, // 30 minutes
}));

// Middleware to load/refresh entitlements
async function ensureEntitlements(req, res, next) {
  const now = Date.now();
  const cacheAge = now - (req.session.entitlementsTimestamp || 0);
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
  
  // Refresh if not cached or cache is stale
  if (!req.session.entitlements || cacheAge > CACHE_TTL) {
    try {
      req.session.entitlements = await validateEntitlements(req.session.userId);
      req.session.entitlementsTimestamp = now;
    } catch (error) {
      console.error('Failed to load entitlements:', error);
      req.session.entitlements = { all: {}, verified: {}, provisional: {} };
    }
  }
  
  next();
}

// Use middleware on protected routes
app.get('/premium-feature', ensureEntitlements, (req, res) => {
  if (req.session.entitlements.all?.freeTrial) {
    res.render('premium-feature');
  } else {
    res.redirect('/upgrade');
  }
});

// Force refresh endpoint
app.post('/api/refresh-entitlements', async (req, res) => {
  try {
    const entitlements = await validateEntitlements(req.session.userId);
    req.session.entitlements = entitlements;
    req.session.entitlementsTimestamp = Date.now();
    res.json(entitlements);
  } catch (error) {
    res.status(500).json({ error: 'Refresh failed' });
  }
});

Automatic Refresh Patterns

On Window Focus

Refresh when user returns to your app:
// Frontend
let lastValidation = Date.now();
const VALIDATION_THRESHOLD = 5 * 60 * 1000; // 5 minutes

window.addEventListener('focus', async () => {
  const timeSinceLastValidation = Date.now() - lastValidation;
  
  if (timeSinceLastValidation > VALIDATION_THRESHOLD) {
    console.log('Window focused after 5 minutes, refreshing entitlements');
    const entitlements = await refreshEntitlements(getCurrentUserId());
    updateUI(entitlements);
    lastValidation = Date.now();
  }
});

After Navigation

Refresh on route changes:
// React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();
  
  useEffect(() => {
    // Refresh on route change
    refreshEntitlements(getCurrentUserId());
  }, [location.pathname]);
  
  return <Routes>...</Routes>;
}

Periodic Polling

Poll for updates at regular intervals:
// Frontend - Simple polling
class EntitlementPoller {
  constructor(userId, intervalMs = 30000) {
    this.userId = userId;
    this.intervalMs = intervalMs;
    this.timerId = null;
  }
  
  async poll() {
    try {
      const response = await fetch('/api/validate-entitlements', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId: this.userId }),
      });
      
      const entitlements = await response.json();
      this.onUpdate?.(entitlements);
    } catch (error) {
      console.error('Poll failed:', error);
    }
  }
  
  start(callback) {
    this.onUpdate = callback;
    this.poll(); // Initial poll
    this.timerId = setInterval(() => this.poll(), this.intervalMs);
  }
  
  stop() {
    if (this.timerId) {
      clearInterval(this.timerId);
      this.timerId = null;
    }
  }
}

// Usage
const poller = new EntitlementPoller('user_123', 30000);
poller.start((entitlements) => {
  console.log('Entitlements updated:', entitlements);
  updateUI(entitlements);
});

// Cleanup
window.addEventListener('beforeunload', () => poller.stop());

Best Practices

1. Don’t Over-Refresh

Respect rate limits and reduce unnecessary API calls:
// ✅ Good - Reasonable refresh interval
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes

// ❌ Bad - Too frequent
const REFRESH_INTERVAL = 1000; // 1 second (way too often!)

2. Cache Appropriately

Balance freshness with performance:
// ✅ Good - 5 minute cache with forced refresh option
async function getEntitlements(userId, forceRefresh = false) {
  if (!forceRefresh) {
    const cached = getCached(userId);
    if (cached && isStillValid(cached)) {
      return cached.data;
    }
  }
  return await validateEntitlements(userId);
}

3. Handle Errors Gracefully

Don’t break user experience on refresh failures:
// ✅ Good - Graceful error handling
async function refreshWithFallback(userId) {
  try {
    return await validateEntitlements(userId);
  } catch (error) {
    console.error('Refresh failed, using cached data:', error);
    return getCachedOrDefault(userId);
  }
}

4. Provide User Feedback

Show loading states during refresh:
// ✅ Good - Loading state
async function handleRefresh() {
  setLoading(true);
  try {
    const entitlements = await refreshEntitlements(userId);
    setEntitlements(entitlements);
    showMessage('Entitlements updated');
  } catch (error) {
    showError('Failed to refresh');
  } finally {
    setLoading(false);
  }
}

5. Refresh After Key Actions

Refresh after actions that might change entitlements:
// ✅ Good - Refresh after offer claimed
async function handleOfferClaimed() {
  const result = await Encore.presentOffer();
  if (result.granted) {
    // Immediately refresh to get new entitlements
    const entitlements = await refreshEntitlements(userId);
    updateUI(entitlements);
  }
}

Complete Example

Here’s a complete implementation with caching and multiple refresh triggers:
// Backend
import express from 'express';
import crypto from 'crypto';

const app = express();
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000;

function generateHmacSignature(privateKey, method, path, body) {
  const bodyJson = JSON.stringify(body);
  const canonical = `${method.toUpperCase()}|${path}|${bodyJson}`;
  const hmac = crypto.createHmac('sha256', privateKey);
  hmac.update(canonical);
  return hmac.digest('hex');
}

async function validateEntitlements(userId) {
  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),
  });
  
  if (!response.ok) {
    throw new Error(`Encore API error: ${response.statusText}`);
  }
  
  return await response.json();
}

app.post('/api/validate-entitlements', async (req, res) => {
  try {
    const { userId, forceRefresh } = req.body;
    const now = Date.now();
    const cached = cache.get(userId);
    
    // Use cache unless forced refresh or stale
    if (!forceRefresh && cached && now - cached.timestamp < CACHE_TTL) {
      return res.json(cached.data);
    }
    
    // Fetch fresh data
    const entitlements = await validateEntitlements(userId);
    cache.set(userId, { data: entitlements, timestamp: now });
    
    res.json(entitlements);
  } catch (error) {
    console.error('Validation error:', error);
    res.status(500).json({ error: 'Validation failed' });
  }
});

app.listen(3000);
// Frontend - React
import { createContext, useContext, useState, useEffect, useCallback } from 'react';

const EntitlementContext = createContext(null);

export function EntitlementProvider({ children, userId }) {
  const [entitlements, setEntitlements] = useState(null);
  const [loading, setLoading] = useState(true);
  
  const refresh = useCallback(async (forceRefresh = false) => {
    setLoading(true);
    try {
      const response = await fetch('/api/validate-entitlements', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, forceRefresh }),
      });
      
      const data = await response.json();
      setEntitlements(data);
    } catch (error) {
      console.error('Failed to refresh:', error);
    } finally {
      setLoading(false);
    }
  }, [userId]);
  
  // Initial load
  useEffect(() => {
    refresh();
  }, [refresh]);
  
  // Refresh on window focus (if stale)
  useEffect(() => {
    let lastRefresh = Date.now();
    
    const handleFocus = () => {
      if (Date.now() - lastRefresh > 5 * 60 * 1000) {
        refresh(true);
        lastRefresh = Date.now();
      }
    };
    
    window.addEventListener('focus', handleFocus);
    return () => window.removeEventListener('focus', handleFocus);
  }, [refresh]);
  
  return (
    <EntitlementContext.Provider value={{ entitlements, loading, refresh }}>
      {children}
    </EntitlementContext.Provider>
  );
}

export function useEntitlements() {
  return useContext(EntitlementContext);
}

Migration from Client-Side Method

If you were using the deprecated client-side method:
// ❌ Old - Client-side refresh (deprecated)
import Encore from '@encore/web-sdk';
await Encore.refreshEntitlements();

// ✅ New - Server-side refresh
const response = await fetch('/api/validate-entitlements', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ 
    userId: getCurrentUserId(),
    forceRefresh: true 
  }),
});
const entitlements = await response.json();

Next Steps