Skip to main content
Validate user entitlements securely on your server to control access to premium features.

Overview

After a user claims an offer, Encore grants them an entitlement (free trial, discount, or credit). For security reasons, entitlement validation must be performed on your backend using server-to-server authentication with HMAC signatures.
Client-side entitlement validation is fundamentally insecure on web platforms. Always validate entitlements on your server before granting access to premium features or processing payments.

Entitlement Types

Encore supports three types of entitlements:
type EntitlementType =
  | { type: 'freeTrial', value?: number, unit?: 'days' | 'months' }
  | { type: 'discount', value?: number, unit?: 'percent' | 'dollars' }
  | { type: 'credit', value?: number, unit?: 'dollars' }
Examples:
// 30-day free trial
{ type: 'freeTrial', value: 30, unit: 'days' }

// 50% discount
{ type: 'discount', value: 50, unit: 'percent' }

// $10 credit
{ type: 'credit', value: 10, unit: 'dollars' }

// Generic free trial (no specific duration)
{ type: 'freeTrial' }

Server-Side Validation

Backend Setup

Set up a secure endpoint on your server to validate entitlements with Encore’s API.
1

Store API Credentials

Store your Encore API keys securely as environment variables.
ENCORE_PUBLIC_API_KEY=your_public_key_here
ENCORE_PRIVATE_API_KEY=your_private_key_here
ENCORE_BASE_URL=https://svc.joinyaw.com/product
Never expose your private API key in client-side code or commit it to version control.
2

Implement HMAC Signature

Create a function to generate HMAC signatures for request authentication.
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');
}
3

Create Validation Endpoint

Create an endpoint on your backend to validate entitlements.
app.post('/api/validate-entitlements', async (req, res) => {
  try {
    const { userId } = req.body;
    
    if (!userId) {
      return res.status(400).json({ error: 'userId is required' });
    }
    
    const method = 'POST';
    const path = '/encore/v1/entitlements/server';
    const body = { user_id: userId };
    
    // Generate HMAC signature
    const signature = generateHmacSignature(
      process.env.ENCORE_PRIVATE_API_KEY,
      method,
      path,
      body
    );
    
    // Generate timestamp for replay attack protection
    const timestamp = Math.floor(Date.now() / 1000);
    
    // Call Encore API
    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}`);
    }
    
    const entitlements = await response.json();
    res.json(entitlements);
  } catch (error) {
    console.error('Validation error:', error);
    res.status(500).json({ error: 'Failed to validate entitlements' });
  }
});

Entitlement Response Format

The Encore API returns entitlements organized by scope:
{
  "all": {
    "freeTrial": {
      "type": "freeTrial",
      "value": 30,
      "unit": "days",
      "scope": "provisional",
      "grantedAt": "2024-01-15T10:00:00Z",
      "expiresAt": "2024-02-14T10:00:00Z",
      "transactionId": "txn_abc123"
    }
  },
  "verified": {},
  "provisional": {
    "freeTrial": {
      "type": "freeTrial",
      "value": 30,
      "unit": "days",
      "scope": "provisional",
      "grantedAt": "2024-01-15T10:00:00Z",
      "expiresAt": "2024-02-14T10:00:00Z",
      "transactionId": "txn_abc123"
    }
  }
}
Scopes:
  • all - Both provisional and verified entitlements
  • verified - Only verified entitlements (confirmed by advertiser)
  • provisional - Only provisional entitlements (not yet verified)

Checking Entitlements

Frontend to Backend Flow

Your frontend should request entitlement validation from your backend:
import { useState, useEffect } from 'react';

function PremiumFeature() {
  const [hasAccess, setHasAccess] = useState(false);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    checkAccess();
  }, []);
  
  const checkAccess = async () => {
    try {
      const response = await fetch('/api/validate-entitlements', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId: getCurrentUserId() }),
      });
      
      const entitlements = await response.json();
      
      // Check for free trial in 'all' scope for instant UX
      setHasAccess(!!entitlements.all?.freeTrial);
    } catch (error) {
      console.error('Failed to validate entitlements:', error);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <LoadingSpinner />;
  if (!hasAccess) return <UpgradePrompt />;
  return <PremiumContent />;
}

When to Use Each Scope

Use ‘all’ for Immediate UX

For the best user experience, grant access immediately when a user claims an offer:
// Check 'all' scope for instant feedback
if (entitlements.all?.freeTrial) {
  unlockPremiumFeatures();
}
This includes both provisional (just claimed) and verified (confirmed) entitlements, providing instant feedback to users.

Use ‘verified’ for Revenue-Critical Decisions

For billing changes or subscription modifications, only use verified entitlements:
// Check 'verified' scope before processing payment
if (entitlements.verified?.discount) {
  applyDiscountToCheckout();
  processPayment();
}
Critical financial operations should only use the verified scope to ensure the conversion has been confirmed by the advertiser.

Use ‘provisional’ for Analytics

Track provisional grants separately:
if (entitlements.provisional?.freeTrial && !entitlements.verified?.freeTrial) {
  analytics.track('provisional_trial_pending_verification');
}

Complete Backend Example

Here’s a complete example with multiple helper functions:
import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

// HMAC signature generation
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');
}

// Validate entitlements endpoint
app.post('/api/validate-entitlements', async (req, res) => {
  try {
    const { userId } = req.body;
    
    if (!userId) {
      return res.status(400).json({ error: 'userId is required' });
    }
    
    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}`);
    }
    
    const entitlements = await response.json();
    res.json(entitlements);
  } catch (error) {
    console.error('Validation error:', error);
    res.status(500).json({ error: 'Failed to validate entitlements' });
  }
});

// Check specific entitlement
app.post('/api/check-entitlement', async (req, res) => {
  try {
    const { userId, entitlementType, scope = 'all' } = req.body;
    
    // Get all entitlements
    const entitlements = await validateEntitlements(userId);
    
    // Check specific entitlement in scope
    const scopeData = entitlements[scope] || {};
    const isActive = scopeData.hasOwnProperty(entitlementType);
    
    res.json({
      isActive,
      entitlementType,
      scope,
      details: isActive ? scopeData[entitlementType] : null,
    });
  } catch (error) {
    console.error('Check entitlement error:', error);
    res.status(500).json({ error: 'Failed to check entitlement' });
  }
});

// Helper function
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.listen(3000, () => {
  console.log('Server running on port 3000');
});

Common Use Cases

Dynamic Feature Access

Gate features based on server-validated entitlements:
async function checkFeatureAccess(userId) {
  const response = await fetch('/api/validate-entitlements', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId }),
  });
  
  const entitlements = await response.json();
  
  // Use 'all' scope for instant access
  return !!entitlements.all?.freeTrial;
}

Checkout Discounts

Apply verified discounts at checkout:
async function calculateCheckoutTotal(userId, items) {
  let total = calculateItemsTotal(items);
  
  const response = await fetch('/api/validate-entitlements', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId }),
  });
  
  const entitlements = await response.json();
  
  // Only apply verified discounts to billing
  const discount = entitlements.verified?.discount;
  if (discount && discount.unit === 'percent') {
    total = total * (1 - discount.value / 100);
  }
  
  return total;
}
Protect premium routes with server validation:
// Express middleware
async function requirePremiumAccess(req, res, next) {
  try {
    const userId = req.session.userId;
    const entitlements = await validateEntitlements(userId);
    
    if (entitlements.all?.freeTrial) {
      next();
    } else {
      res.redirect('/upgrade');
    }
  } catch (error) {
    res.status(500).json({ error: 'Failed to validate access' });
  }
}

app.get('/premium-feature', requirePremiumAccess, (req, res) => {
  res.render('premium-feature');
});

Best Practices

1. Always Validate Server-Side

Never trust client-side validation for access control:
// ✅ Good: Server-side validation
app.post('/api/premium-action', async (req, res) => {
  const entitlements = await validateEntitlements(req.session.userId);
  if (!entitlements.all?.freeTrial) {
    return res.status(403).json({ error: 'Premium access required' });
  }
  // Process premium action
});

// ❌ Bad: Trusting client-provided access status
app.post('/api/premium-action', (req, res) => {
  if (!req.body.hasAccess) {
    return res.status(403).json({ error: 'Access denied' });
  }
  // This is insecure - client can lie about hasAccess
});

2. Cache Validation Results

Cache entitlement validation results to reduce API calls:
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getCachedEntitlements(userId) {
  const cached = cache.get(userId);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }
  
  const entitlements = await validateEntitlements(userId);
  cache.set(userId, { data: entitlements, timestamp: Date.now() });
  return entitlements;
}

3. Use Appropriate Scope

Choose the right scope for your use case:
// Instant UX - use 'all'
if (entitlements.all?.freeTrial) {
  showPremiumFeatures();
}

// Billing - use 'verified'
if (entitlements.verified?.discount) {
  applyDiscountToPayment();
}

4. Handle Validation Errors

Gracefully handle API failures:
async function validateWithFallback(userId) {
  try {
    return await validateEntitlements(userId);
  } catch (error) {
    console.error('Validation failed:', error);
    // Return safe default (no entitlements)
    return { all: {}, verified: {}, provisional: {} };
  }
}

Security Considerations

Critical Security Requirements:
  • Store private API key securely (environment variables, secrets manager)
  • Never expose private key in client-side code or logs
  • Always validate entitlements on your server before granting access
  • Use HTTPS for all API communications
  • Implement rate limiting on validation endpoints
  • Cache validation results appropriately

Next Steps

You’ve learned how to validate entitlements securely on your server. Continue with: For questions, contact admin@joinyaw.com.