Digital Signage Data Feeds & API Integration
Live data feeds transform static digital signage into dynamic, relevant displays that capture attention and provide real value. This comprehensive guide covers all common data integrations with implementation examples.
Why Use Data Feeds?
Benefits of Live Data
| Benefit | Impact |
|---|---|
| Relevance | Content always current and contextual |
| Engagement | Live data captures attention |
| Automation | Reduces manual content updates |
| Personalization | Location/time-specific content |
| Value | Displays become information resources |
Common Data Feed Types
| Category | Examples | Update Frequency |
|---|---|---|
| Weather | Current conditions, forecasts | 15-60 minutes |
| News | Headlines, breaking news | 5-30 minutes |
| Social Media | Feeds, mentions, hashtags | 1-5 minutes |
| Financial | Stock prices, forex, crypto | Real-time to 15 min |
| Sports | Scores, schedules, stats | Real-time during games |
| Transportation | Flight info, transit schedules | Real-time |
| Internal | KPIs, sales data, HR metrics | Minutes to hours |
| Queue/Wait Times | Service counters, attractions | Real-time |
Data Integration Methods
Overview
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Data Source │───▶│ Method │───▶│ Signage Display │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Methods:
├── RSS/Atom Feeds (Pull)
├── REST APIs (Pull)
├── Webhooks (Push)
├── WebSocket (Real-time)
├── MQTT (IoT)
└── Database Direct (Internal)
Method Comparison
| Method | Complexity | Real-time | Best For |
|---|---|---|---|
| RSS/Atom | Low | No (polling) | News, blogs |
| REST API | Medium | No (polling) | Most integrations |
| Webhooks | Medium | Yes (push) | Event triggers |
| WebSocket | High | Yes | Live scores, stocks |
| MQTT | Medium | Yes | IoT sensors |
| Database | High | Variable | Internal data |
Weather Integration
Weather API Providers
| Provider | Free Tier | Features | API Quality |
|---|---|---|---|
| OpenWeatherMap | 1,000 calls/day | Current, forecast, historical | Good |
| WeatherAPI | 1M calls/month | Current, forecast, astronomy | Excellent |
| Tomorrow.io | 500 calls/day | Minute-by-minute, air quality | Excellent |
| Visual Crossing | 1,000 calls/day | Historical, forecast | Good |
| AccuWeather | 50 calls/day | Industry standard | Excellent |
OpenWeatherMap Example
// Weather data fetcher for digital signage
const WEATHER_API_KEY = 'your_api_key';
const CITY = 'New York,US';
const UNITS = 'imperial'; // or 'metric'
async function fetchWeather() {
const url = `https://api.openweathermap.org/data/2.5/weather?q=${CITY}&appid=${WEATHER_API_KEY}&units=${UNITS}`;
try {
const response = await fetch(url);
const data = await response.json();
return {
temperature: Math.round(data.main.temp),
feelsLike: Math.round(data.main.feels_like),
humidity: data.main.humidity,
description: data.weather[0].description,
icon: data.weather[0].icon,
windSpeed: Math.round(data.wind.speed),
city: data.name
};
} catch (error) {
console.error('Weather fetch failed:', error);
return null;
}
}
// Update weather every 30 minutes
setInterval(fetchWeather, 30 * 60 * 1000);
Weather Display Component
<div class="weather-widget">
<div class="weather-icon">
<img src="https://openweathermap.org/img/wn/{{icon}}@2x.png" alt="{{description}}">
</div>
<div class="weather-temp">{{temperature}}°F</div>
<div class="weather-desc">{{description}}</div>
<div class="weather-details">
<span>Feels like {{feelsLike}}°F</span>
<span>Humidity {{humidity}}%</span>
</div>
</div>
Weather Data Binding
Most digital signage CMS platforms support data binding syntax:
Temperature: {{weather.temperature}}°
Conditions: {{weather.description}}
Humidity: {{weather.humidity}}%
Wind: {{weather.windSpeed}} mph
News & RSS Feeds
Popular News RSS Sources
| Source | RSS URL | Update Frequency |
|---|---|---|
| BBC News | feeds.bbci.co.uk/news/rss.xml | Minutes |
| CNN | rss.cnn.com/rss/cnn_topstories.rss | Minutes |
| Reuters | feeds.reuters.com/reuters/topNews | Minutes |
| AP News | apnews.com/apf-topnews/feed | Minutes |
| TechCrunch | feeds.feedburner.com/TechCrunch/ | Hours |
RSS Parser Implementation
// RSS feed parser for signage
async function parseRSSFeed(feedUrl) {
// Use a CORS proxy or server-side fetch
const proxyUrl = `https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(feedUrl)}`;
try {
const response = await fetch(proxyUrl);
const data = await response.json();
return data.items.map(item => ({
title: item.title,
description: stripHtml(item.description),
link: item.link,
pubDate: new Date(item.pubDate),
thumbnail: item.thumbnail || null
}));
} catch (error) {
console.error('RSS parse failed:', error);
return [];
}
}
function stripHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
// Headline ticker configuration
const newsConfig = {
feedUrl: 'https://feeds.bbci.co.uk/news/rss.xml',
maxItems: 10,
refreshInterval: 5 * 60 * 1000, // 5 minutes
displayDuration: 8000 // 8 seconds per headline
};
News Ticker Display
<div class="news-ticker">
<div class="ticker-label">BREAKING NEWS</div>
<div class="ticker-content">
<div class="ticker-item" data-repeat="{{news}}">
<span class="headline">{{title}}</span>
<span class="source">{{source}}</span>
</div>
</div>
</div>
<style>
.news-ticker {
display: flex;
background: #1a1a1a;
color: white;
padding: 10px 20px;
}
.ticker-label {
background: #e00;
padding: 5px 15px;
font-weight: bold;
margin-right: 20px;
}
.ticker-content {
overflow: hidden;
white-space: nowrap;
}
.ticker-item {
display: inline-block;
animation: ticker 20s linear infinite;
}
@keyframes ticker {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
</style>
Social Media Integration
Social Media Sources
| Platform | Integration Method | Considerations |
|---|---|---|
| X (Twitter) | API v2 | Paid tiers, rate limits |
| Basic Display API | Business accounts only | |
| Graph API | Page tokens required | |
| Marketing API | Organization pages | |
| TikTok | Display API | Limited availability |
| YouTube | Data API v3 | Generous free tier |
Social Wall Aggregators
For easier integration, consider social wall services:
| Service | Features | Pricing |
|---|---|---|
| Walls.io | Multi-platform, moderation | From $29/mo |
| Taggbox | Aggregation, analytics | From $19/mo |
| Curator.io | Simple setup, templates | From $25/mo |
| Juicer.io | Free tier available | From $0/mo |
X (Twitter) API v2 Example
// Twitter/X API v2 integration
const BEARER_TOKEN = 'your_bearer_token';
async function fetchTweets(query, maxResults = 10) {
const url = `https://api.twitter.com/2/tweets/search/recent?query=${encodeURIComponent(query)}&max_results=${maxResults}&tweet.fields=created_at,author_id,public_metrics`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${BEARER_TOKEN}`
}
});
const data = await response.json();
return data.data || [];
}
// Fetch tweets with hashtag
const tweets = await fetchTweets('#DigitalSignage');
Instagram Basic Display
// Instagram Basic Display API
const ACCESS_TOKEN = 'your_long_lived_token';
async function fetchInstagramFeed(userId) {
const url = `https://graph.instagram.com/${userId}/media?fields=id,caption,media_type,media_url,permalink,timestamp&access_token=${ACCESS_TOKEN}`;
const response = await fetch(url);
const data = await response.json();
return data.data.filter(item =>
item.media_type === 'IMAGE' || item.media_type === 'CAROUSEL_ALBUM'
);
}
Social Wall Display Layout
<div class="social-wall">
<div class="social-post" data-repeat="{{posts}}">
<div class="post-header">
<img class="avatar" src="{{author.avatar}}" alt="{{author.name}}">
<span class="author-name">{{author.name}}</span>
<span class="platform-icon {{platform}}"></span>
</div>
<div class="post-content">
<p>{{content}}</p>
<img class="post-media" src="{{media}}" alt="" data-if="{{media}}">
</div>
<div class="post-footer">
<span class="timestamp">{{timestamp}}</span>
<span class="engagement">{{likes}} likes</span>
</div>
</div>
</div>
Financial Data
Stock Market APIs
| Provider | Free Tier | Real-time | Best For |
|---|---|---|---|
| Alpha Vantage | 25 calls/day | 1-min delayed | Development |
| Finnhub | 60 calls/min | Yes | Production |
| Polygon.io | Limited | Yes | Professional |
| IEX Cloud | 50k credits/mo | 15-min delay | Business |
| Yahoo Finance | Unofficial | 15-min delay | Testing |
Stock Ticker Implementation
// Stock data fetcher
const FINNHUB_KEY = 'your_api_key';
async function fetchStockQuote(symbol) {
const url = `https://finnhub.io/api/v1/quote?symbol=${symbol}&token=${FINNHUB_KEY}`;
const response = await fetch(url);
const data = await response.json();
return {
symbol: symbol,
price: data.c.toFixed(2),
change: data.d.toFixed(2),
changePercent: data.dp.toFixed(2),
high: data.h.toFixed(2),
low: data.l.toFixed(2),
isUp: data.d >= 0
};
}
// Fetch multiple stocks
async function fetchWatchlist(symbols) {
return Promise.all(symbols.map(fetchStockQuote));
}
// Example usage
const watchlist = await fetchWatchlist(['AAPL', 'GOOGL', 'MSFT', 'AMZN']);
Stock Ticker Display
<div class="stock-ticker">
<div class="stock-item" data-repeat="{{stocks}}">
<span class="symbol">{{symbol}}</span>
<span class="price">${{price}}</span>
<span class="change {{isUp ? 'up' : 'down'}}">
{{isUp ? '▲' : '▼'}} {{changePercent}}%
</span>
</div>
</div>
<style>
.stock-ticker {
display: flex;
background: #000;
color: #fff;
padding: 10px 0;
overflow: hidden;
}
.stock-item {
display: flex;
gap: 10px;
padding: 0 30px;
border-right: 1px solid #333;
}
.symbol {
font-weight: bold;
color: #fff;
}
.price {
color: #ccc;
}
.change.up {
color: #00ff00;
}
.change.down {
color: #ff0000;
}
</style>
Sports Data
Sports API Providers
| Provider | Coverage | Free Tier | Features |
|---|---|---|---|
| ESPN API | US sports | Limited | Scores, schedules |
| API-Football | Global soccer | 100 calls/day | Live scores |
| TheSportsDB | Multi-sport | Free | Basic data |
| Sportradar | Professional | No | Enterprise |
| SofaScore | Multi-sport | Unofficial | Comprehensive |
Live Scores Implementation
// Sports scores fetcher
async function fetchLiveScores(league) {
const url = `https://api-football-v1.p.rapidapi.com/v3/fixtures?live=${league}`;
const response = await fetch(url, {
headers: {
'X-RapidAPI-Key': 'your_api_key',
'X-RapidAPI-Host': 'api-football-v1.p.rapidapi.com'
}
});
const data = await response.json();
return data.response.map(match => ({
homeTeam: match.teams.home.name,
awayTeam: match.teams.away.name,
homeScore: match.goals.home,
awayScore: match.goals.away,
status: match.fixture.status.short,
minute: match.fixture.status.elapsed,
homeLogo: match.teams.home.logo,
awayLogo: match.teams.away.logo
}));
}
Scoreboard Display
<div class="scoreboard">
<div class="match" data-repeat="{{matches}}">
<div class="team home">
<img class="logo" src="{{homeLogo}}" alt="{{homeTeam}}">
<span class="name">{{homeTeam}}</span>
<span class="score">{{homeScore}}</span>
</div>
<div class="match-info">
<span class="status">{{status}}</span>
<span class="time">{{minute}}'</span>
</div>
<div class="team away">
<span class="score">{{awayScore}}</span>
<span class="name">{{awayTeam}}</span>
<img class="logo" src="{{awayLogo}}" alt="{{awayTeam}}">
</div>
</div>
</div>
Transportation Data
Transit & Flight APIs
| Provider | Data Type | Coverage |
|---|---|---|
| FlightAware | Flight tracking | Global |
| FlightRadar24 | Live flights | Global |
| Google Transit | Public transit | Major cities |
| TransitLand | GTFS feeds | Open data |
| Moovit | Public transit | 3,400+ cities |
Flight Information Display
// Flight data for FIDS (Flight Information Display)
async function fetchFlightArrivals(airportCode) {
const url = `https://aeroapi.flightaware.com/aeroapi/airports/${airportCode}/flights/arrivals`;
const response = await fetch(url, {
headers: {
'x-apikey': 'your_api_key'
}
});
const data = await response.json();
return data.arrivals.map(flight => ({
flightNumber: flight.ident,
airline: flight.operator,
origin: flight.origin.city,
scheduledTime: new Date(flight.scheduled_in),
estimatedTime: new Date(flight.estimated_in),
status: flight.status,
gate: flight.gate_destination,
baggage: flight.baggage_claim
}));
}
FIDS Layout
<div class="fids-board arrivals">
<div class="fids-header">
<span>FLIGHT</span>
<span>FROM</span>
<span>SCHEDULED</span>
<span>ESTIMATED</span>
<span>STATUS</span>
<span>GATE</span>
</div>
<div class="fids-row" data-repeat="{{flights}}">
<span class="flight-number">{{flightNumber}}</span>
<span class="origin">{{origin}}</span>
<span class="scheduled">{{scheduledTime | time}}</span>
<span class="estimated">{{estimatedTime | time}}</span>
<span class="status {{statusClass}}">{{status}}</span>
<span class="gate">{{gate}}</span>
</div>
</div>
Custom API Integration
REST API Generic Template
// Generic API data fetcher
class DataFetcher {
constructor(config) {
this.baseUrl = config.baseUrl;
this.headers = config.headers || {};
this.refreshInterval = config.refreshInterval || 60000;
this.transform = config.transform || (data => data);
}
async fetch(endpoint, params = {}) {
const url = new URL(`${this.baseUrl}${endpoint}`);
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
try {
const response = await fetch(url, { headers: this.headers });
const data = await response.json();
return this.transform(data);
} catch (error) {
console.error('API fetch error:', error);
throw error;
}
}
startPolling(endpoint, params, callback) {
const poll = async () => {
const data = await this.fetch(endpoint, params);
callback(data);
};
poll(); // Initial fetch
return setInterval(poll, this.refreshInterval);
}
}
// Usage example
const kpiFetcher = new DataFetcher({
baseUrl: 'https://api.yourcompany.com',
headers: { 'Authorization': 'Bearer your_token' },
refreshInterval: 5 * 60 * 1000, // 5 minutes
transform: data => ({
revenue: data.metrics.daily_revenue,
orders: data.metrics.order_count,
customers: data.metrics.unique_customers
})
});
kpiFetcher.startPolling('/v1/dashboard/metrics', {}, (data) => {
updateDisplay(data);
});
Webhook Receiver
// Express webhook endpoint for real-time updates
const express = require('express');
const WebSocket = require('ws');
const app = express();
const wss = new WebSocket.Server({ port: 8080 });
// Store connected signage clients
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('close', () => clients.delete(ws));
});
// Webhook endpoint
app.post('/webhook/alerts', express.json(), (req, res) => {
const alert = req.body;
// Broadcast to all connected displays
const message = JSON.stringify({
type: 'alert',
data: alert
});
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
res.status(200).send('OK');
});
app.listen(3000);
Data Display Best Practices
Refresh Rates
| Data Type | Recommended Refresh | Rationale |
|---|---|---|
| Weather | 15-30 minutes | Changes slowly |
| News | 5-15 minutes | Timely but not urgent |
| Social media | 1-5 minutes | Engagement important |
| Stock prices | 1-15 minutes | Depends on use case |
| Live sports | 10-30 seconds | Real-time important |
| Flight info | 1-5 minutes | Operational critical |
| Internal KPIs | 5-60 minutes | Depends on metric |
Error Handling
// Robust data fetching with fallbacks
async function fetchWithFallback(primaryUrl, fallbackUrl, cachedData) {
try {
const response = await fetch(primaryUrl, { timeout: 5000 });
if (!response.ok) throw new Error('Primary failed');
return await response.json();
} catch (primaryError) {
console.warn('Primary source failed, trying fallback');
try {
const fallbackResponse = await fetch(fallbackUrl, { timeout: 5000 });
return await fallbackResponse.json();
} catch (fallbackError) {
console.warn('Fallback failed, using cached data');
return cachedData;
}
}
}
// Display stale data indicator
function renderData(data, lastUpdated) {
const isStale = Date.now() - lastUpdated > 10 * 60 * 1000; // 10 minutes
return `
<div class="data-container ${isStale ? 'stale' : ''}">
${renderContent(data)}
${isStale ? '<div class="stale-warning">Data may be outdated</div>' : ''}
</div>
`;
}
Caching Strategy
// Local storage cache for offline resilience
const DataCache = {
set(key, data, ttl = 3600000) { // 1 hour default
const item = {
data,
expiry: Date.now() + ttl,
timestamp: Date.now()
};
localStorage.setItem(`cache_${key}`, JSON.stringify(item));
},
get(key) {
const itemStr = localStorage.getItem(`cache_${key}`);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(`cache_${key}`);
return null;
}
return item;
},
getWithStale(key) {
const itemStr = localStorage.getItem(`cache_${key}`);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
return {
data: item.data,
isStale: Date.now() > item.expiry,
age: Date.now() - item.timestamp
};
}
};
Frequently Asked Questions
Summary
Data feeds make digital signage dynamic and valuable:
- Choose relevant data that provides value to your audience
- Implement proper caching for reliability and offline capability
- Handle errors gracefully with fallbacks and stale indicators
- Respect rate limits to avoid API blocks and extra costs
- Keep displays readable - don't overload with too much live data
With thoughtful integration, live data transforms passive displays into engaging information resources that keep viewers coming back.