Disclosure: I work with the Orbistats team. This post is meant to help you make an honest build-or-embed decision, including the cases where you should not use our widgets.
Every sports product hits the same fork in the road. You have the data, you have a design in your head, and you have a deadline. Do you spend weeks building a match center from scratch, or do you drop in a pre-built one and ship this afternoon?
Most blog posts answer this with "it depends". This one tries to be more useful. We'll go through a concrete decision framework, a small scoring script you can run on your own project, three working architecture patterns (widget-only, custom-only and hybrid), and the details people forget: SEO, performance, layout shift, content security policy and fallbacks.
By the end you'll know exactly when to embed, when to build, and how to do both without regret.
The two options in plain words
Option A is widgets. Orbistats Widgets are drop-in components such as a live score widget, a match center widget and an odds board widget. Your site embeds them, Orbistats powers the data and the UI, and you write very little front-end code.
Option B is a custom UI. You consume the data yourself through the Sports Data API, the Live Scores API and optionally the Odds API, then design and build every pixel.
Both options sit on top of the same data platform, which across 13 sports means football, basketball, American football, cricket, tennis, baseball, esports, combat sports, volleyball, handball, ice hockey, golf and horse racing. That is important because it means you are not locked in. You can start with widgets and move to custom later, or mix them.
What a match center actually contains
When people say "match center" they usually imagine a scoreboard. In reality, a good one is a small application. Here is what a complete one handles:
The header: teams or players, logos, score, status and match clock
The timeline: goals, cards, substitutions, wickets, sets, rounds, depending on the sport
Statistics: possession, shots, corners, aces, rebounds, and so on
Lineups and formations
Head-to-head history
Odds, if your product shows them
Live updates without a page refresh
Edge states: postponed, delayed, abandoned, extra time, penalties, walkovers, retirements
Mobile layout, dark mode, accessibility and translations
Each item looks small on its own. Together they are the reason "we'll just build it ourselves" quietly becomes a six-week project. This is the hidden iceberg of custom sports UI.
The hidden cost of custom UI
Let's be fair to both sides, starting with the cost of building your own.
Sport differences multiply the work. A football timeline and a tennis timeline share almost nothing. Golf and horse racing don't even have a home and away side. If you cover many sports, you are building many match centers.
Live updates need real engineering. Polling is easy to start and expensive to scale. Doing it properly means a WebSocket connection or webhooks, reconnect logic, de-duplication of events and graceful degradation.
Edge cases arrive after launch. Nobody designs the "match abandoned at 63 minutes" screen on day one. Users find it for you.
Maintenance never ends. Every new competition, data field or sport is a ticket in your backlog.
On the other hand, custom UI gives you total control over design, interaction and how data blends with your own product. For some products that control is the whole point.
The hidden cost of widgets
Widgets are not free of trade-offs either.
You get less design freedom. Even a well-themed widget will not behave exactly like a component designed for your product.
Your own data is harder to mix in. If you want to overlay user predictions, wallet balances, fantasy picks or personalised alerts directly inside the match view, an embed is the wrong tool.
SEO needs care. Content rendered inside an embed may not count as your page's content. We'll fix that below.
Embeds touch your security setup. Third-party scripts and frames need to be allowed in your content security policy, and you should plan for what happens when they fail to load.
Plan limits apply. At the time of writing (October 2026), the pricing page positions Growth at $79/month with historical data, widgets and webhooks, while Free is $0 for testing and Starter is $19/month for real-time production. Check which plan includes the widgets you need before you design around them.
A decision framework you can run in 30 seconds
Here is a simple, honest scorecard. Positive numbers push you toward widgets. Negative numbers push you toward a custom build. It is deliberately small so you can edit the weights for your team.
js
// decide.js
// Positive score leans widget. Negative score leans custom.
const FACTORS = [
{ id: "launchFast", ask: "Must it be live in days, not weeks?", yes: 2 },
{ id: "noFrontendCapacity", ask: "No spare front-end developer time?", yes: 2 },
{ id: "contentSite", ask: "Is this for articles, blogs or editorial pages?", yes: 2 },
{ id: "strictBrand", ask: "Must it match a strict design system exactly?", yes: -2 },
{ id: "customInteractions", ask: "Need betslips, favourites, picks or predictions?", yes: -3 },
{ id: "joinOwnData", ask: "Must it blend with your own users or data?", yes: -2 },
{ id: "nativeApp", ask: "Is the main surface a native mobile app?", yes: -3 },
];
export function decide(answers) {
const score = FACTORS.reduce(
(sum, f) => sum + (answers[f.id] ? f.yes : 0),
0
);
if (score >= 3) return { score, verdict: "Use the pre-built widgets" };
if (score <= -2) return { score, verdict: "Build a custom UI" };
return { score, verdict: "Go hybrid: widgets first, custom where it matters" };
}
// Example: a news site adding match pages to articles
console.log(
decide({ launchFast: true, noFrontendCapacity: true, contentSite: true })
);
// { score: 6, verdict: "Use the pre-built widgets" }
// Example: a fantasy app with picks and user data
console.log(
decide({ customInteractions: true, joinOwnData: true, strictBrand: true })
);
// { score: -7, verdict: "Build a custom UI" }
Run it with node decide.js. It is not science, but it forces the conversation your team needs to have, and it makes the answer visible to non-engineers.
Which audience usually lands where
The Orbistats solution pages are organised by audience, and the pattern holds up in practice.
Media and publishers lean heavily toward widgets. If you run editorial pages, Media and Publishers is the natural fit: live scores and match centers on article pages without a front-end project.
Fantasy and analytics products lean custom. The product is the experience, so Fantasy and AI Data teams usually build their own UI on top of the data API.
Sportsbooks and trading products lean custom with real-time feeds. Branding, bet flows and latency matter, so Sportsbooks and Trading teams generally consume odds directly.
Startups and MVPs lean widgets first. Validate that anyone cares about your match pages before investing in a bespoke component library.
Agencies and client sites lean widgets. Fast delivery, low maintenance, predictable result.
Pattern 1: Widget-only (the afternoon build)
If the scorecard said widget, here is how to embed it properly rather than just pasting a snippet and hoping.
First, get access. Create an account, then open the documentation and the widgets page for the exact embed code, supported options and theming controls. The snippet below uses clearly marked placeholders, so replace them with the real values from the docs.
Here is a loader that does three things most copy-pasted embeds don't: it lazy-loads the widget only when it scrolls into view, it reserves space to prevent layout shift, and it never blocks your page.
html
Manchester City vs Arsenal
Premier League, Sunday 18 October 2026, 16:30 UTC
.match-center-slot {
min-height: 520px; /* reserve space so the page does not jump */
border-radius: 12px;
background: rgb(20, 29, 51);
}
// PLACEHOLDER: copy the real script URL and options from the Orbistats widgets docs
const WIDGET_SRC = "WIDGET_SCRIPT_URL_FROM_DOCS";
const slot = document.getElementById("match-center");
const observer = new IntersectionObserver((entries) => {
if (!entries[0].isIntersecting) return;
observer.disconnect();
const s = document.createElement("script");
s.src = WIDGET_SRC;
s.async = true;
document.head.append(s);
// The widget then mounts into the slot as described in the docs.
}, { rootMargin: "200px" });
observer.observe(slot);
Why this matters: a widget at the bottom of a long article might never be seen, and loading it anyway wastes your user's bandwidth. Lazy-loading with a 200px margin means it is ready just before the reader reaches it.
Pattern 2: Custom-only (when the UI is the product)
If you chose custom, you will build on the data API. The safest architecture is the same one from the multi-sport dashboard approach: a small server that holds your API key, caches responses and normalises shapes.
js
// server.js (excerpt)
import "dotenv/config";
import express from "express";
const app = express();
const BASE = "https://api.orbistats.com/v1";
const cache = new Map();
async function orbistats(path, ttlMs) {
const hit = cache.get(path);
if (hit && Date.now() - hit.at < ttlMs) return hit.data;
const res = await fetch(${BASE}${path}, {
headers: {
Authorization: Bearer ${process.env.ORBISTATS_API_KEY},
Accept: "application/json",
},
});
if (!res.ok) throw Object.assign(new Error("Upstream error"), { status: res.status });
const data = await res.json();
cache.set(path, { at: Date.now(), data });
return data;
}
// The live-scores sample on the Orbistats homepage uses this path shape.
app.get("/api/:sport/live", async (req, res, next) => {
try {
res.json(await orbistats(/${req.params.sport}/matches/live, 15000));
} catch (e) { next(e); }
});
app.listen(3000);
And a score header component that renders safely from untrusted text:
js
// matchHeader.js
export function renderMatchHeader(container, match) {
container.replaceChildren();
const status = document.createElement("div");
status.className = "status";
status.textContent =
match.status === "live" ? LIVE ${match.minute}' : match.status;
const row = document.createElement("div");
row.className = "teams";
for (const side of [match.home, match.away]) {
const el = document.createElement("div");
const name = document.createElement("span");
name.textContent = side.name;
const score = document.createElement("strong");
score.textContent = side.score;
el.append(name, score);
row.append(el);
}
container.append(status, row);
}
Field names such as status, minute, home and away follow the homepage sample, so confirm them against the API reference for each sport you cover. You can also use the SDKs if you would rather not write fetch calls by hand, and the Sandbox is the fastest way to look at real responses.
For true live behaviour, replace polling with the WebSocket API or webhooks. A custom match center that polls every few seconds works in a demo and falls over in production.
Pattern 3: The hybrid (what most teams should do)
Here is the opinion I'd stand behind: for most products, the right answer is hybrid.
Your homepage, your hero scoreboard, your favourite-team feed and your highest-converting flows are where design, speed and your own data matter. Build those custom.
Your long tail is different. A basketball game page nobody visits twice, a handball fixture, a golf leaderboard, a volleyball result: these are valuable for coverage and SEO, but they don't justify a bespoke component each. Use the pre-built match center there.
This gives you full coverage across all 13 sports from day one, and you invest design effort only where it pays back.
A configuration object makes the split explicit and easy to change:
js
// matchMode.js
// Sports where you built your own match center. Everything else uses the widget.
const CUSTOM_SPORTS = new Set(["football", "cricket"]);
export function modeFor(sport) {
return CUSTOM_SPORTS.has(sport) ? "custom" : "widget";
}
Add a resilient fallback
Here is the step that separates a professional hybrid from a fragile one. If the widget script fails to load (an ad blocker, a network blip, a blocked origin), your page should still show something useful. Fall back to your own simple score header, which you already built.
js
// mountMatch.js
import { renderMatchHeader } from "./matchHeader.js";
import { modeFor } from "./matchMode.js";
function loadScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.async = true;
s.onload = resolve;
s.onerror = () => reject(new Error("Widget script failed"));
document.head.append(s);
});
}
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Widget timeout")), ms)
),
]);
}
async function renderCustom(slot, sport, matchId) {
const res = await fetch(/api/${sport}/live);
const matches = await res.json();
const match = (matches.data ?? matches).find((m) => m.match_id === matchId);
if (match) renderMatchHeader(slot, match);
else slot.textContent = "Match details are not available right now.";
}
export async function mountMatch(slot) {
const { sport, matchId } = slot.dataset;
if (modeFor(sport) === "custom") {
return renderCustom(slot, sport, matchId);
}
try {
// PLACEHOLDER: use the real widget script URL from the Orbistats docs
await withTimeout(loadScript("WIDGET_SCRIPT_URL_FROM_DOCS"), 4000);
} catch (err) {
console.warn("Falling back to custom header:", err.message);
await renderCustom(slot, sport, matchId);
}
}
The result: widgets do the heavy lifting, but if they fail, users still see the score. That one small function turns an embed from a single point of failure into a progressive enhancement.
Make widgets look like they belong
Whichever pattern you pick, visual consistency decides whether your embed feels native or bolted on. Check the widgets docs for the theming options that are supported, then wrap the slot with your own design tokens so that everything around the widget matches:
css
.match-center-slot {
--surface: rgb(20, 29, 51);
--border: rgb(37, 51, 90);
--accent: rgb(47, 107, 255);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 8px;
min-height: 520px;
}
@media (prefers-color-scheme: light) {
.match-center-slot {
--surface: rgb(255, 255, 255);
--border: rgb(220, 226, 240);
}
}
Even if the widget itself has limited styling, a consistent container, spacing and heading typography around it removes most of the "this is a third-party box" feeling.
SEO: do not let the widget be your only content
This is the mistake that costs publishers the most. If your match page is just a title and an embed, search engines may see an almost empty page, because content rendered inside a script or frame often isn't treated as your content.
The fix is simple. Render the essentials yourself as plain HTML (teams, competition, date, venue and a short summary), and describe the event with structured data:
html
Manchester City vs Arsenal: Match Centre
Follow live score, events and statistics for Manchester City vs Arsenal,
Sunday 18 October 2026, kick-off 16:30 UTC.
{
"@context": "https://schema.org",
"@type": "SportsEvent",
"name": "Manchester City vs Arsenal",
"startDate": "2026-10-18T16:30:00Z",
"sport": "Football",
"homeTeam": { "@type": "SportsTeam", "name": "Manchester City" },
"awayTeam": { "@type": "SportsTeam", "name": "Arsenal" }
}
Now the page has real, indexable text and structured data, while the widget provides the live experience on top. For a site with thousands of match pages across multiple sports, this pattern alone can be the difference between pages that rank and pages that don't.
Performance and security checklist for embeds
Reserve height with min-height so the page doesn't shift when the widget loads.
Lazy-load below-the-fold widgets with IntersectionObserver, as shown above.
Never block rendering: use async or defer for the widget script.
Allow the widget origin in your content security policy. Take the real origin from the docs:
text
Content-Security-Policy:
script-src 'self' https://WIDGET_ORIGIN;
frame-src https://WIDGET_ORIGIN;
connect-src 'self' https://WIDGET_ORIGIN
Keep the API key out of the browser. Widgets handle their own access, and your custom UI should always go through your server.
Add a visible fallback message for when scripts are blocked.
Test with an ad blocker on, because it will happen to real users.
What about cost and request limits?
There is a cost dimension that rarely shows up in comparisons. With a custom UI, every visitor interaction can create load on your server and requests against your API plan, which is why caching matters so much. At the time of writing, the documentation describes 150 requests per day on the Free plan, so you cannot run a public match center on it without aggressive caching. The pricing page lays out the tiers.
With widgets, you should confirm how widget traffic is counted against your plan and which plan unlocks them. Ask before you launch, not after your first traffic spike.
A simple way to think about it: widgets trade design control for lower engineering and operating cost, and custom UI trades engineering cost for control.
How to migrate from widget to custom without a rewrite
Starting with widgets and moving later is a legitimate strategy, as long as you plan for it.
Wrap the widget in your own mount function (like mountMatch above), so the rest of your code never calls the widget directly.
Keep a stable slot element and data attributes for sport and match ID. These are the contract between your page and either implementation.
Track real metrics on widget pages, such as engagement, time on page, bounce rate and conversion.
Replace one sport at a time by adding it to CUSTOM_SPORTS. Because modeFor is the only switch, rollout is a one-line change per sport.
Keep the widget as the fallback for sports you haven't customised.
This is the lowest-risk path, and it lets data (not opinions) decide where to invest your design time.
Common mistakes to avoid
Building a custom match center for all 13 sports on day one, when only two sports drive traffic.
Using a widget as your only page content and wondering why it doesn't rank.
Calling the data API directly from the browser and exposing your key.
Polling every second instead of using WebSockets or webhooks.
Ignoring layout shift, so the page jumps as the widget loads.
Forgetting the failure state, so a blocked script leaves a blank box.
Choosing based on what looks fun to build instead of what the product needs.
Quick recap
Use the pre-built widgets when you need speed, you run editorial or content pages, you have limited front-end capacity, or you want broad coverage across many sports with little maintenance.
Build a custom UI when the experience is the product, you need your own interactions and data inside the match view, or you need strict brand control.
Go hybrid when you want the best of both: custom where it differentiates you, widgets for the long tail, and a fallback so users never see an empty box.
Where to go next
Read the documentation and the quickstart to see how auth and the first request work.
Open the Widgets page to see the components available.
Try real responses in the Sandbox before you commit to a custom build.
Compare tiers on the pricing page.
When you're ready, you can grab a free API key and prototype both approaches side by side. Often an hour with each tells you more than a week of debate.
Over to you: are you team widget, team custom, or team hybrid? And what's the nastiest edge case your match center has ever hit? Share it in the comments, I'd love to compare notes.
Top comments (0)