Skip to main content
Validate user entitlements securely on your server before granting access to premium features or processing payments.
Client-side entitlement validation is fundamentally insecure on web platforms. The Web SDK’s client-side validation methods (isActive(), on(), observeEntitlement()) have been deprecated. Always validate entitlements on your server.

Why Server-Side Validation

Security: Client-side code can be manipulated. Users can modify JavaScript, bypass checks, or spoof entitlement states. Billing Protection: Financial operations (applying discounts, processing payments) must never rely on client-side validation. Fraud Prevention: Server-to-server authentication with HMAC signatures prevents unauthorized access and replay attacks.

Implementation Overview

1

Store API Keys Securely

Add your Encore API keys to 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
2

Generate HMAC Signature

Create a signature function on your backend.
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

Call Encore's Server API

Make authenticated server-to-server requests.
const response = await fetch(`${baseURL}/encore/v1/entitlements/server`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': publicApiKey,
    'X-Signature': signature,
    'X-Timestamp': timestamp.toString(),
  },
  body: JSON.stringify({ user_id: userId }),
});
4

Return Results to Frontend

Send validation results back to your client application.
const entitlements = await response.json();
res.json(entitlements);

API Endpoint

Request

URL: POST /encore/v1/entitlements/server Base URL: https://svc.joinyaw.com/product Headers:
Content-Type
string
required
Must be application/json
X-API-Key
string
required
Your Encore public API key
X-Signature
string
required
HMAC-SHA256 signature of the canonical request string
X-Timestamp
string
required
Unix timestamp in seconds (for replay attack protection)
Body:
user_id
string
required
The user ID to validate entitlements for

Response

Success (200):
{
  "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"
    }
  }
}
all
object
All active entitlements (both provisional and verified)
verified
object
Only verified entitlements (confirmed by advertiser)
provisional
object
Only provisional entitlements (not yet verified)

HMAC Signature Generation

The canonical request string format:
{METHOD}|{PATH}|{JSON_BODY}
Example:
POST|/encore/v1/entitlements/server|{"user_id":"user_123"}

Implementation Examples

import crypto from 'crypto';

function generateHmacSignature(privateKey, method, path, body) {
  // Stringify body without spaces
  const bodyJson = JSON.stringify(body);
  
  // Create canonical string
  const canonical = `${method.toUpperCase()}|${path}|${bodyJson}`;
  
  // Generate HMAC-SHA256 signature
  const hmac = crypto.createHmac('sha256', privateKey);
  hmac.update(canonical);
  return hmac.digest('hex');
}

// Usage
const signature = generateHmacSignature(
  process.env.ENCORE_PRIVATE_API_KEY,
  'POST',
  '/encore/v1/entitlements/server',
  { user_id: 'user_123' }
);

Complete Backend Examples

Node.js / Express

import express from 'express';
import crypto from 'crypto';

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

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');
}

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 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) {
      const errorText = await response.text();
      console.error('Encore API error:', errorText);
      return res.status(response.status).json({
        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' });
  }
});

app.listen(3000);

Python / Flask

from flask import Flask, request, jsonify
import os
import time
import requests
import hmac
import hashlib
import json

app = Flask(__name__)

def generate_hmac_signature(private_key: str, method: str, path: str, body: dict) -> str:
    body_json = json.dumps(body, separators=(',', ':'))
    canonical = f"{method.upper()}|{path}|{body_json}"
    signature = hmac.new(
        private_key.encode('utf-8'),
        canonical.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return signature

@app.route('/api/validate-entitlements', methods=['POST'])
def validate_entitlements():
    try:
        user_id = request.json.get('userId')
        
        if not user_id:
            return jsonify({'error': 'userId is required'}), 400
        
        method = 'POST'
        path = '/encore/v1/entitlements/server'
        body = {'user_id': user_id}
        
        # Generate HMAC signature
        signature = generate_hmac_signature(
            os.environ['ENCORE_PRIVATE_API_KEY'],
            method,
            path,
            body
        )
        
        # Generate timestamp
        timestamp = str(int(time.time()))
        
        # Call Encore API
        response = requests.post(
            f"{os.environ['ENCORE_BASE_URL']}{path}",
            json=body,
            headers={
                'Content-Type': 'application/json',
                'X-API-Key': os.environ['ENCORE_PUBLIC_API_KEY'],
                'X-Signature': signature,
                'X-Timestamp': timestamp,
            }
        )
        
        response.raise_for_status()
        return jsonify(response.json())
        
    except requests.exceptions.HTTPError as e:
        print(f'Encore API error: {e}')
        return jsonify({'error': 'Encore API error'}), response.status_code
    except Exception as e:
        print(f'Validation error: {e}')
        return jsonify({'error': 'Failed to validate entitlements'}), 500

if __name__ == '__main__':
    app.run(port=3000)

PHP / Laravel

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class EntitlementController extends Controller
{
    private function generateHmacSignature($privateKey, $method, $path, $body)
    {
        $bodyJson = json_encode($body);
        $canonical = strtoupper($method) . '|' . $path . '|' . $bodyJson;
        return hash_hmac('sha256', $canonical, $privateKey);
    }
    
    public function validateEntitlements(Request $request)
    {
        $userId = $request->input('userId');
        
        if (!$userId) {
            return response()->json(['error' => 'userId is required'], 400);
        }
        
        $method = 'POST';
        $path = '/encore/v1/entitlements/server';
        $body = ['user_id' => $userId];
        
        // Generate HMAC signature
        $signature = $this->generateHmacSignature(
            env('ENCORE_PRIVATE_API_KEY'),
            $method,
            $path,
            $body
        );
        
        // Generate timestamp
        $timestamp = time();
        
        try {
            // Call Encore API
            $response = Http::withHeaders([
                'Content-Type' => 'application/json',
                'X-API-Key' => env('ENCORE_PUBLIC_API_KEY'),
                'X-Signature' => $signature,
                'X-Timestamp' => (string)$timestamp,
            ])->post(env('ENCORE_BASE_URL') . $path, $body);
            
            if ($response->failed()) {
                return response()->json([
                    'error' => 'Encore API error'
                ], $response->status());
            }
            
            return response()->json($response->json());
            
        } catch (\Exception $e) {
            \Log::error('Validation error: ' . $e->getMessage());
            return response()->json([
                'error' => 'Failed to validate entitlements'
            ], 500);
        }
    }
}

Frontend Integration

Your frontend should call your backend’s validation endpoint:
import { useState, useEffect } from 'react';

function useEntitlementCheck(userId, entitlementType, scope = 'all') {
  const [hasAccess, setHasAccess] = useState(false);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    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();
        const scopeData = entitlements[scope] || {};
        setHasAccess(!!scopeData[entitlementType]);
      } catch (error) {
        console.error('Validation failed:', error);
        setHasAccess(false);
      } finally {
        setLoading(false);
      }
    };
    
    checkAccess();
  }, [userId, entitlementType, scope]);
  
  return { hasAccess, loading };
}

// Usage
function PremiumFeature() {
  const { hasAccess, loading } = useEntitlementCheck(
    getCurrentUserId(),
    'freeTrial',
    'all'
  );
  
  if (loading) return <LoadingSpinner />;
  if (!hasAccess) return <UpgradePrompt />;
  return <PremiumContent />;
}

Using Entitlement Scopes

’all’ Scope - Instant UX

Use for immediate user feedback:
// Backend check
const entitlements = await validateEntitlements(userId);
if (entitlements.all?.freeTrial) {
  // Grant immediate access
  return { hasAccess: true };
}

‘verified’ Scope - Revenue Operations

Use for billing and payments:
// Backend check before processing payment
const entitlements = await validateEntitlements(userId);
if (entitlements.verified?.discount) {
  // Apply discount to payment
  const discount = entitlements.verified.discount;
  if (discount.unit === 'percent') {
    total = total * (1 - discount.value / 100);
  }
}

‘provisional’ Scope - Analytics

Track pending verifications:
const entitlements = await validateEntitlements(userId);
const hasProvisional = !!entitlements.provisional?.freeTrial;
const hasVerified = !!entitlements.verified?.freeTrial;

if (hasProvisional && !hasVerified) {
  // Track pending verification
  analytics.track('entitlement_pending_verification', { userId });
}

Best Practices

1. Secure Your Private Key

Never expose your private API key:
  • Store in environment variables
  • Don’t commit to version control
  • Don’t send to client
  • Don’t log in plain text
  • Rotate regularly
// ✅ Good
const privateKey = process.env.ENCORE_PRIVATE_API_KEY;

// ❌ Bad - hardcoded
const privateKey = 'sk_live_abc123...';

// ❌ Bad - sent to client
res.json({ privateKey: process.env.ENCORE_PRIVATE_API_KEY });

2. Cache Validation Results

Reduce API calls by caching results:
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. Handle Errors Gracefully

Fail securely when validation fails:
async function checkAccessWithFallback(userId) {
  try {
    const entitlements = await validateEntitlements(userId);
    return !!entitlements.all?.freeTrial;
  } catch (error) {
    console.error('Validation failed:', error);
    // Fail closed - deny access on error
    return false;
  }
}

4. Use Appropriate Scopes

Match scope to use case:
// ✅ Good - instant UX with 'all'
app.get('/feature', async (req, res) => {
  const entitlements = await validateEntitlements(req.userId);
  if (entitlements.all?.freeTrial) {
    return res.render('premium-feature');
  }
  return res.redirect('/upgrade');
});

// ✅ Good - billing with 'verified'
app.post('/checkout', async (req, res) => {
  const entitlements = await validateEntitlements(req.userId);
  if (entitlements.verified?.discount) {
    applyDiscount(entitlements.verified.discount);
  }
  processPayment();
});

5. Implement Rate Limiting

Protect your validation endpoint:
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute
  max: 60, // 60 requests per minute
  message: 'Too many validation requests',
});

app.post('/api/validate-entitlements', limiter, validateEntitlements);

Troubleshooting

Invalid Signature Error

Problem: Receiving 401 or 403 errors Solutions:
  • Verify canonical string format: METHOD|PATH|BODY
  • Ensure JSON body has no spaces: {"user_id":"123"}
  • Check private key is correct
  • Confirm timestamp is in seconds (not milliseconds)
// ✅ Correct
const bodyJson = JSON.stringify({ user_id: 'user_123' });
// Result: {"user_id":"user_123"}

// ❌ Wrong - has spaces
const bodyJson = JSON.stringify({ user_id: 'user_123' }, null, 2);
// Result: {
//   "user_id": "user_123"
// }

Timestamp Errors

Problem: Request rejected due to timestamp Solutions:
  • Use Unix timestamp in seconds
  • Ensure server clocks are synchronized
  • Check timestamp is current (within acceptable window)
// ✅ Correct - seconds
const timestamp = Math.floor(Date.now() / 1000);

// ❌ Wrong - milliseconds
const timestamp = Date.now();

Empty Entitlements

Problem: Validation returns empty objects Solutions:
  • Verify user has claimed offers
  • Check user ID matches
  • Confirm entitlements haven’t expired
  • Validate API keys are correct

Migration from Client-Side Methods

If you were using deprecated client-side methods:
// ❌ Old - Client-side (deprecated)
import Encore from '@encore/web-sdk';
if (Encore.isActive({ type: 'freeTrial' })) {
  showPremiumContent();
}

// ✅ New - Server-side validation
// Frontend
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) {
  showPremiumContent();
}

// Backend
app.post('/api/validate-entitlements', async (req, res) => {
  const entitlements = await validateEntitlements(req.body.userId);
  res.json(entitlements);
});

Next Steps