When a publisher recently asked if Zype offered an embeddable Electronic Program Guide (EPG) widget for websites, the short answer was: not yet. But that didn’t mean they were out of options. In fact, with just a bit of creativity—and the help of Zype’s Playout EPG APIs and a commercial LLM like ChatGPT—I was able to build exactly what they needed in under 30 minutes.
This wasn’t a team of frontend developers with weeks of runway. It was one person (me), a linear stream powered by Zype’s Playout service, and a simple goal: display a real-time program guide on an existing website, styled to match a specific brand, without a full product build.
The Challenge: A Custom Guide Without the Custom Work
Zype Playout already supports EPGs across connected TV and mobile apps through its Apps Creator platform (you can view reference docs here). But on the web, we find some publishers want tighter control: their own HTML experience, integrated directly into their site’s layout and SEO framework.
Rather than wait for a productized embeddable widget or hire out custom development for an ask like this, I experimented with a rapid prototyping approach using ChatGPT.
Every Zype Playout stream includes an XMLTV feed—an industry-standard format that lists program schedules, titles, descriptions, and more. It’s the same structured metadata used by many OTT platforms to display program guides.
With this feed in hand, I dropped it into ChatGPT and simply asked:
“Can you help me create an HTML-based EPG using this XMLTV file?”
The response: a resounding yes!
After just a few iterations, I had a working, styled HTML+JavaScript EPG snippet—ready to embed directly into a live site.
Here are some prompt examples I used during my conversation:
The final result wasn’t a generic iframe. It was a real, responsive program guide that could be styled, extended, and embedded anywhere. And because it relied solely on standard web tech and Zype’s APIs, there were no dependencies or lock-in—just clean code that worked with an existing publishing environment.
For teams using Zype Playout to power linear streams, this example highlights what’s possible when an API-first platform meets modern no-code tools. With just Zype’s XMLTV EPG API feed and a commercial LLM like ChatGPT, teams can quickly spin up a custom, branded EPG—without waiting on roadmap items or investing in full-scale frontend development.
This kind of flexibility is especially valuable for smaller teams who need to move fast, maintain UX control, and avoid third-party dependencies. It’s a glimpse into the future of building: rapid, iterative, and powered by composable APIs and AI.
If you’re interested in learning more about how Zype’s APIs can help you create efficient video operations, request a meeting with our team and we’ll be happy to discuss your specific needs. If you’d like to experiment with implementing your own EPG on your website, here is the full code that ChatGPT provided for you to try.
Note: In order to make this code work, you need to provide a Zype API Key and Playout Channel ID!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Free Speech TV</title>
<style>
body { font-family: Arial, sans-serif; background-color: #000; color: #fff; }
.epg-container { width: 100%; overflow-x: auto; padding: 20px; display: flex; flex-direction: row; position: relative; }
.epg-header { display: flex; justify-content: center; align-items: center; padding: 10px 20px; background: #111; font-size: 18px; font-weight: bold; }
.epg-info { display: flex; justify-content: space-between; padding: 10px; background: #222; margin-bottom: 10px; align-items: center; }
.epg-section { flex: 1; padding: 10px; text-align: center; background: #333; margin: 5px; border-radius: 5px; }
.epg-section h2 { font-size: 16px; margin-bottom: 5px; }
.epg-row { display: flex; flex-direction: column; align-items: center; padding: 10px; margin-right: 10px; border-radius: 5px; background: #222; min-width: 250px; position: relative; }
.epg-program { font-weight: bold; margin-bottom: 5px; text-align: center; font-size: 18px; }
.epg-time { font-size: 14px; color: #bbb; margin-bottom: 5px; }
.epg-description { font-size: 12px; color: #ddd; text-align: center; margin-top: 5px; }
.progress-bar { width: 100%; height: 5px; background: #444; position: relative; margin-top: 5px; }
.progress-bar .progress { height: 100%; background: #9147ff; width: 0%; transition: width 1s linear; }
</style>
</head>
<body>
<div class="epg-header">Now: <span id="currentTime"></span></div>
<div class="epg-info">
<div class="epg-section">
<h2>Currently Playing</h2>
<div id="currentProgramTitle">Loading...</div>
<div id="currentProgramTime"></div>
<div class="progress-bar"><div class="progress" id="progressCurrent"></div></div>
</div>
<div class="epg-section">
<h2>Next</h2>
<div id="nextProgramTitle">Loading...</div>
<div id="nextProgramTime"></div>
</div>
<div class="epg-section">
<h2>Later</h2>
<div id="laterProgramTitle">Loading...</div>
<div id="laterProgramTime"></div>
</div>
</div>
<div class="epg-container" id="epgContainer"></div>
<script>
async function loadEPG() {
const response = await fetch('https://api.zype.com/scheduler/v1/channels/{PLAYOUT_CHANNEL_ID}/published/rundown/xmltv.xml?api_key={ZYPE_API_KEY}&mode=hybrid&hours=336');
const text = await response.text();
const parser = new DOMParser();
const xml = parser.parseFromString(text, "text/xml");
const epgContainer = document.getElementById("epgContainer");
const programs = xml.getElementsByTagName("programme");
const nowEpoch = new Date().getTime();
let currentProgram = null;
let nextProgram = null;
let laterProgram = null;
for (let i = 0; i < programs.length; i++) {
const program = programs[i];
const title = program.getElementsByTagName("title")[0].textContent;
const descriptionElement = program.getElementsByTagName("desc")[0];
const description = descriptionElement ? descriptionElement.textContent : "No description available";
const startEpoch = parseInt(program.getAttribute("start"));
const stopEpoch = parseInt(program.getAttribute("stop"));
const startTime = formatEpochTime(startEpoch);
const endTime = formatEpochTime(stopEpoch);
if (nowEpoch >= startEpoch && nowEpoch < stopEpoch) {
currentProgram = { title, startTime, endTime, startEpoch, stopEpoch };
} else if (startEpoch > nowEpoch && !nextProgram) {
nextProgram = { title, startTime, endTime };
} else if (startEpoch > nowEpoch && !laterProgram && nextProgram.title !== title) {
laterProgram = { title, startTime, endTime };
}
const epgRow = document.createElement("div");
epgRow.classList.add("epg-row");
epgRow.innerHTML = `
<div class="epg-program">${title}</div>
<div class="epg-time">${startTime} - ${endTime}</div>
<div class="epg-description">${description}</div>
`;
epgContainer.appendChild(epgRow);
}
if (currentProgram) {
document.getElementById("currentProgramTitle").textContent = currentProgram.title;
document.getElementById("currentProgramTime").textContent = `${currentProgram.startTime} - ${currentProgram.endTime}`;
updateProgressBar(currentProgram.startEpoch, currentProgram.stopEpoch);
}
if (nextProgram) {
document.getElementById("nextProgramTitle").textContent = nextProgram.title;
document.getElementById("nextProgramTime").textContent = `${nextProgram.startTime} - ${nextProgram.endTime}`;
}
if (laterProgram) {
document.getElementById("laterProgramTitle").textContent = laterProgram.title;
document.getElementById("laterProgramTime").textContent = `${laterProgram.startTime} - ${laterProgram.endTime}`;
}
}
function formatEpochTime(epoch) {
const date = new Date(epoch);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function updateProgressBar(startEpoch, stopEpoch) {
const nowEpoch = new Date().getTime();
const progress = ((nowEpoch - startEpoch) / (stopEpoch - startEpoch)) * 100;
document.getElementById("progressCurrent").style.width = `${Math.min(100, Math.max(0, progress))}%`;
}
setInterval(updateProgressBar, 1000);
loadEPG();
</script>
</body>
</html>