Why Publishers Need Real Odds Data (And Why Most Get It Wrong)
You run a major sports news site. Annually, you get 50M visitors. Your readers expect one thing: accurate, current odds from their favorite sportsbooks.
But here's what happens: You embed odds widgets or call an odds API, and the data is 2-5 seconds old. Meanwhile, readers are refreshing your site and complaining that the odds don't match what they see in their sportsbook app.
Worse, during major events (Super Bowl, World Cup Final, Champions League playoff), your odds widget timing out or showing stale data makes your site look broken.
The problem isn't the widget. It's that most publishers are trying to integrate odds APIs the way they integrate weather APIs (occasional updates are fine). Sports odds are different. They update 100+ times per minute during live play. Your integration architecture needs to reflect this.
This guide walks through the technical and business decisions publishers face when integrating odds data.
The Publisher's Odds Integration Problem
As a publisher, you face a unique constraint: you're not the bookmaker. You don't control the odds. You're just displaying them.
This creates several problems:
Problem 1: Trust and Accuracy If your odds are wrong, readers assume your entire site is untrustworthy. Showing odds that are 30 seconds stale on a fast-moving market is functionally wrong.
Problem 2: Liability and Compliance You're showing odds from multiple sportsbooks. If your odds display is incorrect and a reader reports they made a bet based on your stale odds, you have liability exposure. You need audit trails showing when odds were updated and which source they came from.
Problem 3: User Experience Widgets that flicker, APIs that time out, or loading states that persist—all degrade user experience. During peak events, when users are most engaged, is when your infrastructure is under the most load.
Problem 4: Monetisation Your newsroom wants to drive clicks to sportsbooks (affiliate revenue). But affiliate revenue only works if your odds are competitive and current. If your odds are stale, users navigate away to get real-time data elsewhere.
Integration Architecture Decision: Widget vs. API vs. Hybrid
The first decision: how do you want to show odds?
Option 1: Embedded Widget (Iframe-Based)
How it works: You embed an iframe on your page that pulls from our hosted odds widget. We handle all updates, caching, and reliability.
Architecture:
<iframe src="https://odds.fairplay.com/widget/event/123456?theme=light"></iframe>
Pros:
- Zero maintenance on your side
- We handle all updates and performance
- Automatic compliance compliance (data audit trails)
- Works on any platform (web, mobile web, even email)
- High-quality UI out of the box
Cons:
- Less customization (you're bound by our UI)
- Slight latency overhead (iframe rendering takes 50-100ms)
- Limited ability to style to match your brand
- Dependent on our infrastructure (if we have issues, your widget is affected)
- You're not learning customer preferences (all interaction data stays with us)
When to use: News and guide sites where off-the-shelf quality matters more than customization. La Gazzetta, MARCA, and most major sports news sites use this model.
Cost model: Usually $0.01-0.05 per widget embed per month, plus revenue share on affiliate clicks.
Option 2: REST API (Pull-Based)
How it works: Your frontend calls our API every 5-30 seconds requesting current odds. You render them yourself.
Architecture:
// Your frontend JavaScript
async function updateOdds() {
const response = await fetch(`/api/odds/events/${eventId}`);
const odds = await response.json();
renderOdds(odds);
}
setInterval(updateOdds, 5000); // Poll every 5 seconds
Pros:
- Full customization of UI
- You control the refresh rate
- Easy to integrate with your existing frontend
- Can combine with other data sources (news, stats)
- Better SEO (odds data is in your HTML, not an iframe)
Cons:
- Higher latency (5-30 second refresh rate is typical)
- More bandwidth (polling means repeated requests)
- You're responsible for error handling, fallbacks, loading states
- More engineering work to build and maintain
- Higher failure rate (network issues, user device issues)
When to use: Publisher content where odds are secondary to news content. You want customization but don't need <1 second latency.
Cost model: Usually $5K-15K/month per deployment, plus API call pricing ($0.001-0.01 per request).
Technical considerations:
You'll need to handle:
- Cache invalidation: Odds can change rapidly. How often do you poll? If every 5 seconds, you're making 17,280 API calls per day per event. Multiply by 100 concurrent events, that's 1.7M calls/day. At $0.005/call, that's $8.5K/month just in API costs.
- Error handling: API times out. What do you show? Stale odds or an error message?
- Fallback display: Multiple sportsbooks might be available. If one fails to update, do you hide that sportsbook or show stale data?
Option 3: WebSocket (Push-Based)
How it works: Your frontend opens a persistent WebSocket connection to our server. We push odds updates to you as they happen.
Architecture:
const ws = new WebSocket('wss://odds.fairplay.com/stream/events');
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
renderOdds(update);
};
Pros:
- Lowest latency (50-200ms between update and user sees it)
- Lower bandwidth (only send what changed, not entire odds snapshot)
- Real-time feel (odds update live on the screen)
- Reduced API call load
- Better UX during peak events
Cons:
- Higher engineering complexity (WebSocket state management)
- More complex error handling (connections can drop)
- Requires more infrastructure (persistent connections use more memory)
- Browser compatibility (very old browsers don't support WebSocket)
- Harder to debug (request/response debugging is harder with streaming)
When to use: Premium content where real-time odds are core to the user experience. Dedicated odds pages, live betting guides, in-play commentary.
Cost model: Usually $15K-30K/month, as we need to maintain persistent connections for you.
Technical considerations:
// Proper WebSocket with reconnection logic
class OddsStream {
constructor() {
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.connect();
}
connect() {
this.ws = new WebSocket('wss://odds.fairplay.com/stream/events');
this.ws.onopen = () => {
this.reconnectAttempts = 0;
console.log('Connected to odds stream');
};
this.ws.onmessage = (event) => {
const update = JSON.parse(event.data);
this.handleUpdate(update);
};
this.ws.onerror = () => {
this.attemptReconnect();
};
this.ws.onclose = () => {
this.attemptReconnect();
};
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => this.connect(), delay);
}
}
handleUpdate(update) {
// Your rendering logic here
}
}
Option 4: Hybrid (Widget + API Fallback)
How it works: Primary display uses embedded widget for reliability. Fallback uses API if widget loads slowly.
When to use: You want the reliability of widgets but the flexibility of custom display.
Cost model: Combination of widget and API pricing.
Choosing Your Integration Path: Decision Framework
Use this decision tree:
Do you need sub-second latency?
├─ Yes → Use WebSocket or Widget
│ └─ Do you need full UI customization?
│ ├─ Yes → WebSocket (high engineering effort)
│ └─ No → Widget (low engineering effort)
│
└─ No (5-30 second latency is acceptable)
└─ Do you need custom UI?
├─ Yes → REST API (medium engineering effort)
└─ No → Widget (low engineering effort)
Implementation Deep-Dive: REST API Pattern
Most publishers start with REST API because it feels familiar. Here's how to implement it correctly.
API Design
GET /api/v2/odds/events/{eventId}
Query params:
- format: decimal|fractional|moneyline (default: decimal)
- sportsbooks: comma-separated list (default: all available)
- markets: comma-separated market IDs (default: all)
- timestamp: include last-updated timestamp (default: true)
Response:
{
"eventId": "event_123456",
"eventName": "Manchester United vs. Liverpool",
"sport": "soccer",
"timestamp": "2026-03-23T15:30:45.123Z",
"sportsbooks": [
{
"id": "bet365",
"name": "bet365",
"markets": [
{
"id": "match_winner",
"name": "Match Winner",
"outcomes": [
{
"name": "Manchester United",
"odds": 2.15,
"lastUpdated": "2026-03-23T15:30:44.987Z"
},
{
"name": "Draw",
"odds": 3.50,
"lastUpdated": "2026-03-23T15:30:40.123Z"
},
{
"name": "Liverpool",
"odds": 3.25,
"lastUpdated": "2026-03-23T15:30:44.112Z"
}
]
}
]
}
]
}
Polling Strategy
The naive approach: poll every 5 seconds. This is expensive and creates load spikes at :00, :05, :10, etc.
Better approach: adaptive polling
class AdaptiveOddsPoller {
constructor() {
this.pollInterval = 5000; // Start at 5 seconds
this.lastUpdateTime = 0;
this.updatesSinceLastPoll = 0;
this.minInterval = 3000; // Don't poll faster than 3s
this.maxInterval = 60000; // Don't poll slower than 60s
}
async poll() {
const response = await this.fetchOdds();
const now = Date.now();
// If we're getting updates on every poll, increase frequency
if (this.updatesSinceLastPoll > 0) {
this.pollInterval = Math.max(this.pollInterval * 0.8, this.minInterval);
} else {
// No updates, decrease frequency
this.pollInterval = Math.min(this.pollInterval * 1.2, this.maxInterval);
}
this.updatesSinceLastPoll = 0;
return response;
}
}
This reduces API calls by 40-60% during low-activity periods while maintaining responsiveness during peak action.
Error Handling
async function updateOdds(eventId) {
try {
const response = await Promise.race([
fetch(`/api/v2/odds/events/${eventId}`),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
)
]);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const odds = await response.json();
renderOdds(odds);
// Clear error state if we recover
clearErrorDisplay();
} catch (error) {
handleOddsError(error);
// Show stale odds or error message, not blank space
}
}
function handleOddsError(error) {
if (error.message === 'Timeout') {
showStaleOddsWithWarning('Odds are delayed');
} else if (error.message.includes('API error')) {
showError('Unable to load odds. Using cached data.');
} else {
showError('Odds unavailable.');
}
}
Implementation Deep-Dive: WebSocket Pattern
WebSocket is more complex but worth it for premium content.
Connection Management
class RobustOddsStream {
constructor(config) {
this.url = config.url;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.maxReconnectAttempts = Infinity;
this.subscriptions = new Map();
this.connected = false;
this.reconnectAttempts = 0;
}
connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('[OddsStream] Connected');
this.connected = true;
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
// Resubscribe to all events
this.subscriptions.forEach((config, eventId) => {
this.subscribe(eventId, config);
});
resolve();
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
console.error('[OddsStream] Parse error:', error);
}
};
this.ws.onerror = (error) => {
console.error('[OddsStream] Error:', error);
this.handleError(error);
};
this.ws.onclose = () => {
console.log('[OddsStream] Disconnected');
this.connected = false;
this.attemptReconnect();
};
} catch (error) {
reject(error);
}
});
}
subscribe(eventId, config = {}) {
this.subscriptions.set(eventId, config);
if (this.connected) {
this.ws.send(JSON.stringify({
type: 'subscribe',
eventId,
...config
}));
}
}
handleMessage(message) {
switch (message.type) {
case 'odds_update':
this.onOddsUpdate(message.data);
break;
case 'event_state_change':
this.onStateChange(message.data);
break;
default:
console.warn('[OddsStream] Unknown message type:', message.type);
}
}
attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[OddsStream] Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
const delay = Math.min(this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), this.maxReconnectDelay);
console.log(`[OddsStream] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect().catch(error => {
console.error('[OddsStream] Reconnection failed:', error);
});
}, delay);
}
}
Memory Management
Long-lived WebSocket connections can leak memory if not managed carefully.
// Avoid adding event listeners directly to the WebSocket
// Instead, use a manager pattern:
class MessageHandler {
constructor() {
this.handlers = new Map();
}
on(type, handler) {
if (!this.handlers.has(type)) {
this.handlers.set(type, []);
}
this.handlers.get(type).push(handler);
// Return unsubscribe function
return () => {
const handlers = this.handlers.get(type);
const index = handlers.indexOf(handler);
if (index > -1) handlers.splice(index, 1);
};
}
emit(type, data) {
const handlers = this.handlers.get(type);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
clear(type) {
if (type) {
this.handlers.delete(type);
} else {
this.handlers.clear();
}
}
}
Performance Considerations
Rendering Performance
Don't re-render the entire odds table on every update. Update only what changed:
function renderOddsUpdate(update) {
const outcomeElement = document.getElementById(`outcome-${update.outcomeId}`);
if (outcomeElement) {
// Only update the odds value, not the entire row
const oddsSpan = outcomeElement.querySelector('.odds-value');
const newValue = formatOdds(update.odds);
if (oddsSpan.textContent !== newValue) {
oddsSpan.textContent = newValue;
// Add animation class for visual feedback
oddsSpan.classList.add('odds-updated');
setTimeout(() => oddsSpan.classList.remove('odds-updated'), 500);
}
}
}
Network Optimisation
Keep requests small and responses focused:
// Don't request all markets if you only show 5
const params = new URLSearchParams({
eventId: 123456,
markets: 'match_winner,over_under_2.5,both_to_score',
sportsbooks: 'bet365,betfair,draftkings'
});
fetch(`/api/v2/odds?${params}`);
Caching Strategy
class OddsCache {
constructor(ttl = 5000) {
this.cache = new Map();
this.ttl = ttl;
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return item.value;
}
set(key, value) {
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
// Cleanup expired items
cleanup() {
const now = Date.now();
for (const [key, item] of this.cache.entries()) {
if (now - item.timestamp > this.ttl) {
this.cache.delete(key);
}
}
}
}
Compliance and Auditing
Publishers have compliance obligations even though they're not taking bets:
- Data Provenance: Know which sportsbook each odds display came from
- Timestamp Accuracy: Record when odds were received and displayed
- User Data: If you track clicks on sportsbook links, ensure GDPR compliance
- Responsible Gambling: Display responsible gambling messaging appropriately
// Log every odds display for audit purposes
function logOddsDisplay(eventId, sportsbook, odds, timestamp) {
const audit = {
eventId,
sportsbook,
odds,
displayTimestamp: new Date(),
sourceTimestamp: timestamp,
userId: getCurrentUserId(), // If applicable
page: window.location.pathname
};
// Send to your audit logging backend
fetch('/api/audit/odds-display', {
method: 'POST',
body: JSON.stringify(audit),
headers: { 'Content-Type': 'application/json' }
});
}
Advanced Integration Patterns for Scale
As your publisher site grows, you'll face scale challenges. Here are proven patterns:
Pattern 1: Distributed Caching with Redis
For high-traffic publishers (100M+ monthly visitors), use distributed cache:
// Redis-backed odds cache
class DistributedOddsCache {
constructor(redisClient) {
this.redis = redisClient;
this.localCache = new Map(); // Local backup cache
}
async getOdds(eventId, marketId) {
const key = `odds:${eventId}:${marketId}`;
// Try local cache first (fastest)
let odds = this.localCache.get(key);
if (odds && !this.isStale(odds)) {
return odds;
}
// Try Redis (faster than API)
try {
const cached = await this.redis.get(key);
if (cached) {
odds = JSON.parse(cached);
this.localCache.set(key, odds);
return odds;
}
} catch (error) {
console.warn('Redis failure, using local cache', error);
return this.localCache.get(key);
}
// Fallback to API
const fresh = await this.fetchFromAPI(eventId, marketId);
await this.redis.setex(key, 5, JSON.stringify(fresh)); // Cache for 5 seconds
this.localCache.set(key, fresh);
return fresh;
}
isStale(odds) {
const age = Date.now() - odds.timestamp;
return age > 5000; // 5 second TTL
}
async fetchFromAPI(eventId, marketId) {
const response = await fetch(`/api/odds/${eventId}/${marketId}`);
return response.json();
}
}
This pattern reduces API load by 90% during peak traffic.
Pattern 2: Smart Prefetching
Prefetch odds for events likely to be viewed:
class OddsPrefetcher {
constructor(apiClient, cache) {
this.api = apiClient;
this.cache = cache;
}
// Prefetch odds for trending events
async prefetchTrending() {
const trending = await this.getTrendingEvents();
for (const event of trending) {
const odds = await this.api.getOdds(event.id);
// Store in cache
await this.cache.set(event.id, odds);
}
}
// Prefetch odds for events on homepage
async prefetchHomepage() {
const homepage = await this.getHomepageEvents();
// Prefetch top 5 events in parallel
await Promise.all(
homepage.slice(0, 5).map(event =>
this.api.getOdds(event.id)
)
);
}
// Run periodically
startAutoRefresh() {
setInterval(() => this.prefetchTrending(), 10000); // Every 10 seconds
setInterval(() => this.prefetchHomepage(), 30000); // Every 30 seconds
}
}
Pattern 3: Smart Request Batching
For publishers showing 100+ odds on a page:
class BatchOddsRequester {
constructor(apiClient) {
this.api = apiClient;
this.batch = [];
this.batchTimer = null;
}
request(eventId, marketId) {
return new Promise((resolve, reject) => {
this.batch.push({ eventId, marketId, resolve, reject });
// Batch requests that come within 50ms
if (this.batchTimer) clearTimeout(this.batchTimer);
this.batchTimer = setTimeout(() => this.flush(), 50);
});
}
async flush() {
if (this.batch.length === 0) return;
const batch = this.batch;
this.batch = [];
try {
const eventIds = [...new Set(batch.map(b => b.eventId))];
const marketIds = batch.map(b => b.marketId);
// Single API call for all odds
const response = await this.api.getOddsBatch(eventIds, marketIds);
const oddsMap = this.indexResponse(response);
// Resolve all individual requests
batch.forEach(b => {
const odds = oddsMap[`${b.eventId}:${b.marketId}`];
b.resolve(odds);
});
} catch (error) {
batch.forEach(b => b.reject(error));
}
}
indexResponse(response) {
const map = {};
response.forEach(item => {
map[`${item.eventId}:${item.marketId}`] = item;
});
return map;
}
}
Instead of 100 API calls, this makes 1-2 batched calls.
Real-World Performance Optimisation Results
Publisher benchmarks after implementing these patterns:
Before optimisation:
- Page load time: 4.2 seconds
- API calls per pageload: 50+
- Cache hit rate: 0%
- Server response time: 800ms
After optimisation:
- Page load time: 2.1 seconds (2x faster)
- API calls per pageload: 3-5 (90% reduction)
- Cache hit rate: 95%
- Server response time: 100ms
Results in business metrics:
- Bounce rate down 23%
- Session duration up 45%
- Pages per session up 38%
- Mobile conversions up 52%
These optimisations matter. They directly translate to user engagement and revenue.
Monitoring and Observability
For production publishers, you need detailed monitoring:
class OddsMetricsCollector {
constructor() {
this.metrics = {
apiCalls: 0,
cacheHits: 0,
cacheMisses: 0,
apiLatencies: [],
errors: 0
};
}
trackApiCall(latency) {
this.metrics.apiCalls++;
this.metrics.apiLatencies.push(latency);
// Send to metrics service every 60 calls
if (this.metrics.apiCalls % 60 === 0) {
this.reportMetrics();
}
}
trackCacheHit() {
this.metrics.cacheHits++;
}
trackCacheMiss() {
this.metrics.cacheMisses++;
}
trackError(error) {
this.metrics.errors++;
console.error('Odds API error:', error);
}
reportMetrics() {
const avgLatency = this.metrics.apiLatencies.length > 0
? this.metrics.apiLatencies.reduce((a, b) => a + b) / this.metrics.apiLatencies.length
: 0;
const cacheHitRate = (this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)) * 100;
fetch('/api/metrics/odds', {
method: 'POST',
body: JSON.stringify({
timestamp: new Date(),
apiCalls: this.metrics.apiCalls,
cacheHitRate,
avgLatency,
errors: this.metrics.errors
})
});
// Reset metrics
this.metrics = {
apiCalls: 0,
cacheHits: 0,
cacheMisses: 0,
apiLatencies: [],
errors: 0
};
}
}
Set alerts:
- Cache hit rate < 80%: Something is wrong
- Average latency > 500ms: Potential API bottleneck
- Error rate > 1%: Data quality issue
Moving Forward: Integrating with FairPlay's Odds API
FairPlay's odds API for publishers includes:
- Multiple format support: REST, WebSocket, Widget, or custom integration
- Real-time data: 125M price changes daily from 50+ sources
- Compliance built-in: Audit trails, data provenance, regional compliance
- Developer support: SDKs for JavaScript, React, Vue, and custom integrations
- Monitoring: We monitor your integration and alert if there are latency issues
- Caching infrastructure: Edge caches in 4 global regions to reduce latency
- Batching API: Designed for high-volume publisher requests
We've integrated with 50+ publishers including major sports news sites across Europe and North America. Average integration time: 2-4 weeks. Integration cost: $20-50K depending on complexity.
Success Metrics to Track
After going live, track these metrics monthly:
- Bounce rate: Should decrease by 10-20% (odds help engagement)
- Avg session duration: Should increase 20-40% (users stay longer)
- Pages per session: Should increase 15-30%
- Affiliate revenue: Should increase 30-50% (better odds drive clicks)
- SEO visibility: Should improve over 6 months (odds-related keywords)
Publishers who implement odds optimally see 40-60% revenue uplift within 6 months.
Next Steps
- Evaluate your use case: Are you building a news site, betting guide, or live odds display?
- Choose your integration path: Widget, REST, WebSocket, or hybrid?
- Plan for scale: Start with simple caching, scale to distributed cache as traffic grows
- Load test: Make sure your infrastructure handles peak traffic (10x normal load)
- Implement: Use the code patterns in this guide
- Monitor: Set up alerts for latency, cache hit rate, and error rate
- Optimise: Based on metrics, adjust caching strategy and refresh rates
Let's get your odds data live and generating revenue.
Related Articles:
Ready to explore BetTech for your business?
Talk to the FairPlay team about how our platform can work for your business.
Get Started








