Skip to content

Host-fed event data ​

Two widgets can render a full sporting event with no socket, no REST call, and no @gomagaming/sports-domain connection at all: you push the whole event as a plain object and the widget expands it into the same store shape a live feed would have produced. This page is the contract for that object.

When to use it ​

Every other data path documented elsewhere in these guides assumes GomaPulse can reach a betting provider: dataSource: 'subscribe' opens a WAMP connection over socketUrl, dataSource: 'rpc' makes a one-shot call. Some deployments don't have that option: a CMS-driven page where the CMS is the only source of match data, with no sports-API connection anywhere in the stack. For that case, two widgets accept event data as a plain host-supplied object instead of a subscription:

  • <goma-hero-card>, via its hostEvent property. The full contract below: identity, teams, live state, and as many markets as you supply.
  • <goma-multi-banners>, on a type: 'sport' banner item, two ways:
    • the event key: one whole GomaHostEvent object, expanded the same way hostEvent is on <goma-hero-card> (the card takes markets[0] as its single odds row). This is the only path that gets a live sport banner a real score and clock.
    • the flat inline fields: the single-market subset of the same field vocabulary (one market per banner, inlined directly on the item instead of nested in a markets array). Still fully supported, just without score / clock.

Both still need dataSource/the banner shape set correctly and the odds buttons still emit goma:outcome-select / drive the shared betslip exactly as a live feed would: the only thing that's different is where the event data originates.

The GomaHostEvent contract ​

Authority

This type block is read directly off packages/sports-domain/src/host-feed/normalizeHostEvent.js. The pure mapper both widgets go through when using this contract. <goma-hero-card> calls it via hostEvent; <goma-multi-banners> calls it too, via a sport banner's event key: its flat inline fields are the one path that doesn't call it, and those mirror the fields below one for one instead. If a field isn't listed here, the mapper doesn't read it, and setting it does nothing.

ts
type GomaHostEvent = {
  id?: string | number              // event identity: also the betslip's eventId
  homeTeam?: string
  awayTeam?: string
  competition?: string              // e.g. "Premier League"
  sportId?: number                  // default 1 if omitted
  venueId?: string | number         // drives the region/country glyph
  venueName?: string                // fallback label for the same glyph
  competitionId?: string | number

  startTime?: number                // epoch ms
  isLive?: boolean                  // strictly === true; anything else is prelive
  score?: { home?: number, away?: number }
  clock?: { minute?: number, part?: string }

  markets?: Array<{
    id?: string                     // optional: see the positional-fallback note below
    name?: string                   // display name, e.g. "Match Result"
    bettingTypeId?: string | number // see the warning below: omit at your peril
    eventPartId?: string | number
    mainLine?: boolean              // default true
    outcomes?: Array<{
      id?: string                   // optional: see the note below
      label?: string                // renders as the odds-button text
      odds: number                  // decimal
      bettingOfferId?: string
      isAvailable?: boolean         // strictly `false` disables the outcome
    }>
  }>
}

Every field in that block is marked optional because the mapper is written defensively: it never throws on a missing field, it just produces a correspondingly empty or placeholder value. That does not mean every field is optional in practice: an event with no id has no betslip identity, a market with no bettingTypeId cannot be placed (see below), and an outcome with no odds leaves the button stuck in a perpetual loading spinner rather than showing a price. Treat the type as "will not crash," not as "safe to omit."

Field-by-field semantics ​

id doubles as the betslip's event identity: the shared odds-button component reads event?.id (the mapper's game.id, stringified) straight into the eventId field of goma:outcome-select and goma:event-navigate. Supply a stable id per event; if you omit it the mapper leaves game.id as null and every selection off that event carries a null eventId.

homeTeam / awayTeam are plain display strings: no participant ids are read from the host payload, and none should be sent. The mapper stamps its own internal synthetic participant ids onto the game object and the score row (host-home / host-away) purely so its own score-extraction logic can tell which side is which; those ids are never compared against anything the CMS supplies and are not part of this contract: don't try to set them, and don't expect them back on any emitted event.

competition becomes the header's competition/tournament name (game.parentName).

sportId defaults to 1 when omitted and drives the bundled sport glyph shown in the header (resolved from GomaPulse's own icon set, you don't supply an icon URL for this).

venueId / venueName drive the region/country glyph next to the sport icon, resolved the same way: off @gomagaming/core's bundled flag mapping, not a URL you provide. venueId is tried first; venueName is a fallback label for the same lookup. (This is unrelated to <goma-hero-card>'s separate homeFlag / awayFlag properties, see the "host-fed imagery" row of field applicability by widget below.)

competitionId is carried onto game.competitionId and not otherwise transformed by the mapper.

startTime is an epoch-millisecond timestamp. <goma-hero-card> formats it into a date/time string itself, through the same shared composable every other widget's prelive card uses: there's no separate "date label" input on the hero card side; you give it one timestamp and it does the rest.

isLive is checked with === true: anything else (including a truthy-but-non-boolean value) is treated as prelive. It switches the header/primary-slide between the live score+clock display and the start-time display.

score: { home, away }. A score row is only produced when at least one of home / away is not null/undefined; omit score entirely (or leave both sides unset) for an event that hasn't started yet. 0 is a valid score on either side: it is not coerced away.

clock: { minute, part }. minute is 0-safe too: the mapper checks != null, not truthiness, so a match at kickoff (minute: 0) displays correctly instead of falling back to "no time." part defaults to the literal string '-' when omitted. Never null, because a downstream formatter calls .replace() on it and a null there throws.

markets[]: one entry per market. id is optional; when you omit it, the mapper assigns a positional id (host-market-0, host-market-1, …) purely so that two id-less markets in the same array don't collapse into a single row downstream. Treat that fallback as an internal safety net, not something to build against: supply a real id whenever you have one. name becomes the market's display name everywhere a renderer looks for one (the mapper writes it under three different internal keys for that reason, which is a note about the implementation, not something you need to mirror on input). mainLine (default true) matters when two markets in your array resolve to the same betting-type + event-part combination: <goma-hero-card>'s market de-duplicator keeps whichever one has mainLine: true, so set it to false on an alternate line you don't want winning that tie-break.

markets[].outcomes[]: label is what renders on the odds button. id is optional; the mapper synthesizes ${marketId}:${label} when it's missing, which means two outcomes with the same label in the same market will collide if you don't give them explicit ids: always set id when a market could plausibly have duplicate labels (e.g. two "Over"/"Under" lines under one betting type). odds is a decimal number. It goes through Number(), so a non-numeric value produces NaN, which the odds button treats as "no price yet": rather than failing loudly, the button renders a perpetual loading spinner instead of a value. isAvailable disables the outcome (greyed out, unselectable, aria-disabled) when the value is the literal boolean false; every other value. Including the string'false'. Is treated as available, because the check is a strict !== false, not a truthiness or type coercion.

bettingTypeId: read this before you ship anything ​

A market without bettingTypeId cannot be placed

bettingTypeId identifies which kind of market this is to the betting provider. It is not decorative metadata. A betslip leg built from a market that never had one is refused at place-bet with:

"Invalid or missing Betting Type Id on placed bet"

The mapper cannot invent this value; there is no live feed to fall back to in host-fed mode, unlike dataSource: 'subscribe', where the widget resolves the market itself. When your payload omits it, the mapper logs exactly one console warning per market, naming it. The block below is shortened: it shows only the start of the warning. The full line then says that the market's odds will be rejected at place-bet and points to the host-fed data guide:

[goma] host event market "<name or id>" has no bettingTypeId

That warning fires at the moment the payload enters the store (long before anyone taps an odds button) specifically so it surfaces during CMS content review, not during a live incident. Every market you intend to be placeable must carry bettingTypeId. If you don't know the provider's betting-type taxonomy, get it from whoever owns the sports-data integration before publishing the content: there is no client-side substitute for it.

eventPartId travels with bettingTypeId as its pairing key (together they form the market's displayKey, ${bettingTypeId}_${eventPartId}): supply it whenever the same betting type can apply to more than one part of the match (e.g. 1st half vs. whole match), so the two don't get treated as the same market.

Two worked payloads ​

A prelive football match, one market ​

js
const heroCard = document.querySelector('goma-hero-card')
heroCard.dataSource = 'prop'
heroCard.hostEvent = {
  id: '900123456789012345',
  homeTeam: 'Arsenal',
  awayTeam: 'Chelsea',
  competition: 'Premier League',
  sportId: 1,
  venueId: 'GB',
  venueName: 'England',
  competitionId: '4001',
  startTime: 1789000000000,
  isLive: false,
  markets: [
    {
      id: 'm-1x2',
      name: 'Match Result',
      bettingTypeId: 69,
      eventPartId: 3,
      mainLine: true,
      outcomes: [
        { id: 'o-home', label: 'Arsenal', odds: 2.10, bettingOfferId: 'bo-home-1', isAvailable: true },
        { id: 'o-draw', label: 'Draw',    odds: 3.40, bettingOfferId: 'bo-draw-1', isAvailable: true },
        { id: 'o-away', label: 'Chelsea', odds: 3.20, bettingOfferId: 'bo-away-1', isAvailable: true },
      ],
    },
  ],
}
heroCard.heroImage = 'https://cdn.example.com/arsenal-chelsea.jpg'

(Sport, betting-type and venue ids above are illustrative, use your own provider's values.)

A live basketball match, score + clock + three markets ​

js
const heroCard = document.querySelector('goma-hero-card')
heroCard.dataSource = 'prop'
heroCard.hostEvent = {
  id: '900987654321098765',
  homeTeam: 'Real Madrid',
  awayTeam: 'Fenerbahce',
  competition: 'EuroLeague',
  sportId: 3,
  venueId: 'ES',
  venueName: 'Spain',
  competitionId: '5002',
  startTime: 1789003600000,
  isLive: true,
  score: { home: 58, away: 52 },
  clock: { minute: 27, part: '3rd Quarter' },
  markets: [
    {
      id: 'm-winner',
      name: 'Match Winner',
      bettingTypeId: 219,
      eventPartId: 0,
      mainLine: true,
      outcomes: [
        { id: 'o-home', label: 'Real Madrid', odds: 1.65, bettingOfferId: 'bo-h-1', isAvailable: true },
        { id: 'o-away', label: 'Fenerbahce',  odds: 2.20, bettingOfferId: 'bo-a-1', isAvailable: true },
      ],
    },
    {
      id: 'm-total',
      name: 'Total Points Over/Under 165.5',
      bettingTypeId: 225,
      eventPartId: 0,
      mainLine: true,
      outcomes: [
        { id: 'o-over',  label: 'Over 165.5',  odds: 1.90, bettingOfferId: 'bo-o-1', isAvailable: true },
        { id: 'o-under', label: 'Under 165.5', odds: 1.90, bettingOfferId: 'bo-u-1', isAvailable: false },
      ],
    },
    {
      id: 'm-handicap',
      name: 'Handicap',
      bettingTypeId: 231,
      eventPartId: 0,
      mainLine: true,
      outcomes: [
        { id: 'o-home-hc', label: 'Real Madrid -4.5', odds: 1.85, bettingOfferId: 'bo-hh-1', isAvailable: true },
        { id: 'o-away-hc', label: 'Fenerbahce +4.5',  odds: 1.95, bettingOfferId: 'bo-ah-1', isAvailable: true },
      ],
    },
  ],
}
heroCard.heroImage = 'https://cdn.example.com/rm-fener.jpg'
heroCard.marketCount = 5

Every market above carries bettingTypeId deliberately: drop any one of them to see the console warning described above, and to reproduce the place-bet rejection it's warning you about.

Field-applicability by widget ​

<goma-multi-banners> has three ways to fill a sport banner. Only two of them are host-fed, so only those two appear in the table below, but you need all three in view to choose between them:

ModeYou supplyWhat it can expressNeeds a connection
eventIdan event id, nothing elseEverything, live: the widget subscribes and the score and clock genuinely tickyes
eventone GomaHostEvent object, nested under the event keyEverything except live movement: the score and clock render, but change only when you push a new objectno
flat inlinethe match fields spread directly on the banner itemTeams, time, one market. No score or clock exist in this shape, so a live card shows no scoreno

All three render the same card. They differ in where the data comes from and what the shape is capable of saying.

Pick event unless you have a reason not to: it is the same vocabulary <goma-hero-card> takes, so one CMS payload feeds both widgets, and it is the only host-fed mode that can express a score at all. The flat shape is supported and not going away, but it is the older one and cannot say everything the newer one can.

Resolution order when more than one is present: eventId beats event beats the flat fields. A live feed beats a snapshot, and a snapshot beats loose fields.

Field group<goma-hero-card> (hostEvent)<goma-multi-banners> (event)<goma-multi-banners> (flat inline, no event)
identity, teams, competition (id, homeTeam, awayTeam, competition)yesyesyes (when eventId is also omitted, see the multi-banners docs)
sportIdyes, default 1yes, default 1yes, default 1
startTimeyes: auto-formatted internally, no separate label fieldsyes, auto-formatted the same wayyes, or the explicit dateLabel + timeLabel strings instead
isLiveyesyesyes
score (live)yes: { home, away }available: { home, away }, same shape as hero-card and rendered by the same score machinery. A snapshot: it changes when you assign a new object, it does not tick by itselfnot available. A flat live card shows no score; use event for a host-controlled score, or eventId for one that ticks
clock (live)yes: { minute, part }available: { minute, part }, same caveat/behaviour as score abovenot available, same caveat as score
markets[]all of them, capped at marketCount (default 5)all of them accepted, but the card renders only markets[0] (one odds row, same cap as the flat shape below)exactly one market: its fields (marketId, marketName, bettingTypeId, eventPartId, outcomes[]) are inlined directly on the banner item, not nested in a markets array
host-fed imageryseparate properties, not part of hostEvent: heroImage, homeFlag, awayFlagbackgroundImage on the banner item (not part of event)backgroundImage on the banner item

How each widget takes it ​

<goma-hero-card>: set dataSource = 'prop' and assign the whole object to the hostEvent property (or the host-event attribute as a JSON string):

js
el.hostEvent = { id: '900123456789012345', homeTeam: 'Arsenal', /* … */ }
html
<goma-hero-card
  host-event='{"id":"900123456789012345","homeTeam":"Arsenal","awayTeam":"Chelsea"}'
></goma-hero-card>

Setting hostEvent back to null via the JS property hands control back to the raw props described below: whatever they currently hold, including nothing. Removing the host-event attribute does not do this: like every attribute on a GomaPulse custom element, attributeChangedCallback only reacts to a value being set, not removed, so a stale hostEvent stays in place until you clear it from JS with el.hostEvent = null (or assign a new object).

<goma-multi-banners>: there is no separate property; you build the sport-type object directly inside the banners array, alongside your casino/info banners. Two ways to feed it:

Nest a whole GomaHostEvent under event for a real score/clock and several markets (the card takes markets[0]):

js
el.banners = [
  {
    id: 's1', type: 'sport', backgroundImage: '…',
    event: {
      id: '900123456789012345', homeTeam: 'Arsenal', awayTeam: 'Chelsea',
      isLive: true, score: { home: 1, away: 0 }, clock: { minute: 60, part: '2nd Half' },
      markets: [{ id: 'm1', name: 'Match Result', bettingTypeId: 69, eventPartId: 3, outcomes: [/* … */] }],
    },
  },
]

Or use the flat inline fields from the table above (no event key, no score / clock):

js
el.banners = [
  { id: 's1', type: 'sport', backgroundImage: '…', homeTeam: 'Arsenal', awayTeam: 'Chelsea',
    bettingTypeId: 69, eventPartId: 3, outcomes: [/* … */] },
]

See the multi-banners widget page for the full MultiBannerItem type, including the exact three-mode precedence order (eventId beats event beats the flat fields), and the casino and info banner shapes this contract doesn't cover.

Escape hatch: raw provider-shaped props (advanced) ​

<goma-hero-card> still accepts its older raw props (event, markets, outcomes, bettingOffers) which map straight into the same four store slots hostEvent expands into, with no normalization step. These are the advanced path: for a host that already holds data shaped like the live feed's own records (for example, a proxy that replays cached provider payloads), skipping normalizeHostEvent avoids a pointless round-trip through a shape it would just have to re-derive.

hostEvent always wins when both are set: assigning it replaces whatever the raw props currently hold. Clearing it (hostEvent = null) hands control back to the raw props immediately, including "nothing," if that's what they currently hold. Most integrations should reach for hostEvent; only use the raw props if you're already working with provider-shaped objects and specifically want to bypass the mapper.

  • Shared state: once a host-fed event's odds buttons are tapped, the selection travels through the same goma:betslip:v4 envelope a live feed's selections do.
  • Cross-widget wiring: the full goma:* event catalogue, including goma:outcome-select and goma:event-navigate, both of which a host-fed card emits identically to a live one.
  • <goma-hero-card> / <goma-multi-banners>: the full per-widget config, props and events.