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
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
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');
}
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 }),
});
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:
Your Encore public API key
HMAC-SHA256 signature of the canonical request string
Unix timestamp in seconds (for replay attack protection)
Body:
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 active entitlements (both provisional and verified)
Only verified entitlements (confirmed by advertiser)
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