Introduction
SmatchONE Odds est un fournisseur de données sportives : cotes pregame et live, résultats et scores en direct, issus de notre back office d'agrégation. L'API expose un schéma stable versionné v1. Chaque ressource est normalisée et accompagnée d'une enveloppe de traçabilité _provenance.
Format : JSON UTF-8. Cotes en format décimal. Dates en ISO 8601. Les horodatages de match (startsAt) sont exprimés dans le fuseau configuré pour votre compte (par défaut heure du Cameroun, UTC+1) ; les horodatages techniques (_provenance.fetchedAt) restent en UTC. Voir la section Fuseaux horaires.
Base URL & Authentification
Base URL : https://real-odds-api-production.up.railway.app Toutes les routes /agd/v1/* exigent un header : Authorization: Bearer <token-fourni-à-l-onboarding> La route /health est ouverte (pas de token).
Le token est remis lors de l'onboarding. Ne le partagez pas côté client.
curl -H "Authorization: Bearer <token-fourni-à-l-onboarding>" \ "https://real-odds-api-production.up.railway.app/agd/v1/fixtures?status=LIVE&limit=50"
Fuseaux horaires
Les horodatages de match (champ startsAt des fixtures) sont émis en ISO 8601 avec offset de fuseau — le fuseau configuré pour votre compte, par défaut l'heure du Cameroun (Africa/Douala, UTC+1).
"startsAt": "2026-06-26T18:55:00+01:00" // 18h55 heure du Cameroun // = exactement le même instant que "2026-06-26T17:55:00Z" (UTC)
C'est le même instantqu'en UTC, simplement exprimé dans le fuseau de votre compte. Un parseur ISO 8601 standard (datetime()) accepte indifféremment le suffixe Z ou l'offset ±HH:MM : aucun changement de code requis côté intégration.
À retenir : seuls les horodatages de match sont exprimés dans votre fuseau. Les horodatages techniques — notamment _provenance.fetchedAt — restent toujours en UTC (suffixe Z).
Enveloppe _provenance
Chaque objet renvoyé porte un champ _provenance décrivant l'origine de la donnée, sa fraîcheur et ses éventuelles lacunes.
"_provenance": {
"source": "the-odds-api", // fournisseur d'origine
"fetchedAt": "2026-06-26T19:31:21Z", // horodatage de récupération (UTC)
"rawId": "3", // identifiant brut côté source
"missing": ["logoUrl"], // champs absents dans la source
"warnings": [], // alertes de normalisation
"schemaVersion": "v1" // version du schéma
}Sports
GET https://real-odds-api-production.up.railway.app/agd/v1/sports
[
{
"id": "football",
"key": "football",
"name": "Football",
"isActive": true,
"_provenance": { "source": "the-odds-api", "fetchedAt": "2026-06-26T19:31:21Z",
"rawId": "3", "missing": [], "warnings": [], "schemaVersion": "v1" }
}
]Ligues
GET https://real-odds-api-production.up.railway.app/agd/v1/leagues?limit=1000
[
{
"id": "4033",
"sportId": "football",
"countryId": null,
"name": "Kategoria Superiore",
"season": "2026",
"type": "league",
"logoUrl": null,
"_provenance": { "source": "the-odds-api", "rawId": "4033", "missing": ["countryId","logoUrl"],
"warnings": [], "schemaVersion": "v1" }
}
]Équipes
GET https://real-odds-api-production.up.railway.app/agd/v1/teams?limit=1000
[
{
"id": "aj-deem",
"name": "AJ Deem",
"countryId": null,
"logoUrl": null,
"founded": null,
"venue": null,
"_provenance": { "source": "the-odds-api", "rawId": "aj-deem", "schemaVersion": "v1" }
}
]Matchs & Résultats
Les matchs sont filtrables par statut. Un résultat est simplement un match au statut FINISHED avec ses scores (homeScore / awayScore).
Coup d'envoi. startsAt est un ISO 8601 avec offset de fuseau (ex. 2026-06-26T18:55:00+01:00), correspondant au fuseau configuré pour votre compte (par défaut heure du Cameroun, UTC+1). C'est le même instant qu'en UTC — voir Fuseaux horaires.
Statut fiable. Le champ status n'est pas le statut brut du bookmaker (qui reste figé sur « live »). Il est recalculé à partir de plusieurs signaux réels, par ordre de fiabilité :
SCHEDULED— le coup d'envoi (startsAt) n'a pas encore eu lieu.FINISHED— dès qu'un de ces signaux est vrai : résultat final reçu (score FT), ou tous les marchés du match sont fermés (plus aucun pari possible), ou match commencé depuis plus de 4 h (garde-fou). Un match terminé est immédiatement settlable.LIVE— match commencé, dans la fenêtre de jeu, avec au moins un marché encore ouvert. Un matchLIVEest réellement en cours (il ne reste jamais « live » des heures après la fin).CANCELLED/POSTPONED/PAUSED— match annulé/abandonné, reporté, ou interrompu temporairement. Émis dès que le bookmaker le signale, pour que tu puisses annuler/suspendre les tickets concernés.
Scores mi-temps. homeHalftimeScore / awayHalftimeScore sont renseignés quand la donnée mi-temps est disponible. Sinon ils valent null et apparaissent dans _provenance.missing.
GET https://real-odds-api-production.up.railway.app/agd/v1/fixtures?status=LIVE|SCHEDULED|FINISHED&limit=100
GET https://real-odds-api-production.up.railway.app/agd/v1/fixtures/{id}{
"id": "5541815",
"leagueId": "43221",
"homeTeamId": "aj-deem",
"awayTeamId": "boland-brendan",
"startsAt": "2026-06-26T18:55:00+01:00", // fuseau du compte (déf. UTC+1) = 17:55:00Z
"status": "FINISHED",
"season": "2026",
"homeTeamName": "AJ Deem",
"awayTeamName": "Boland, Brendan",
"leagueName": "UTR PTT Waco Men 01",
"homeScore": 2,
"awayScore": 1,
"homeHalftimeScore": 1, // renseigné si la mi-temps est connue, sinon null
"awayHalftimeScore": 0,
"_provenance": { "source": "the-odds-api", "rawId": "5541815",
"missing": [], "schemaVersion": "v1" }
}Exemple sans mi-temps disponible : homeHalftimeScore / awayHalftimeScore à null, listés dans _provenance.missing :
{
"id": "5541900",
"status": "FINISHED",
"homeScore": 3,
"awayScore": 2,
"homeHalftimeScore": null,
"awayHalftimeScore": null,
"_provenance": { "source": "the-odds-api", "rawId": "5541900",
"missing": ["homeHalftimeScore", "awayHalftimeScore"],
"schemaVersion": "v1" }
}Cotes (pregame + live)
Les cotes sont regroupées par marché (clés canoniques) puis par sélection. Clés canoniques courantes : match-winner, over-under-X, both-teams-score, double-chance, asian-handicap.
GET https://real-odds-api-production.up.railway.app/agd/v1/odds/{id}{
"fixtureId": "5541815",
"bookmakerId": "betconstruct",
"markets": [
{
"key": "winner-of-the-match",
"name": "Winner of the match",
"status": "OPEN",
"description": { "fr": "Résultat final : 1 = domicile · X = nul · 2 = extérieur.", "en": "Final result: 1 = home · X = draw · 2 = away." },
"margin": 15.0,
"selections": [
{ "key": "home", "label": "AJ Deem", "decimalOdds": 2.1275, "line": null, "status": "ACTIVE" },
{ "key": "away", "label": "Boland, Brendan", "decimalOdds": 2.2425, "line": null, "status": "ACTIVE" }
]
}
],
"_provenance": { "source": "the-odds-api", "schemaVersion": "v1" }
}Livescore
GET https://real-odds-api-production.up.railway.app/agd/v1/livescore/{id}Renvoie le score en direct quand disponible, sinon { "detail": "no livescore for fixture" }.
Scores par période. periodScores est rempli quand l'information de période existe : un tableau de { period, home, away } où home / away sont les points marqués dans cette période (non cumulés). Valeurs possibles de period : 1H, 2H, HT (foot), Q1…Q4 (basket), S1…S5 (tennis). Le champ period(période courante) est renseigné s'il est connu.
{
"fixtureId": "5541815",
"homeScore": 1,
"awayScore": 0,
"minute": 34,
"period": "2H", // période courante si connue, sinon null
"periodScores": [ // rempli si l'info de période existe, sinon []
{ "period": "1H", "home": 1, "away": 0 },
{ "period": "2H", "home": 0, "away": 0 }
],
"_provenance": { "source": "the-odds-api", "schemaVersion": "v1" }
}WebSocket temps réel — /agd/v1/ws
UNE seule connexion WebSocket pousse tout en temps réel — cotes, livescore, minute, statut. Aucun GET REST nécessaire pour le temps réel. Le SSE /stream et les GET REST restent disponibles en parallèle (migration).
wss://real-odds-api-production.up.railway.app/agd/v1/ws
Auth : le même token Bearer, au choix — header Authorization: Bearer <token>sur l'upgrade, OU sous-protocole agd.v1.bearer.<token>. Une seule connexion par instance suffit (pas de connexion par match).
1) Abonnement (client → serveur)
{ "type": "subscribe",
"channels": ["odds", "livescore", "fixture"],
"filter": { "sport": "football" } }channels : omettez pour tout recevoir. filter.sport : optionnel.
2) Snapshot puis flux (serveur → client)
Juste après l'ack, on burst l'état completde tous les matchs de l'abonnement, puis on passe au flux live. 0 appel REST pour être cohérent au démarrage (idem à la reconnexion).
{ "type": "subscribed", "channels": [...], "snapshotStart": true }
… odds.update / livescore.update / fixture.update (état courant) …
{ "type": "snapshotEnd" }
… flux live …3) Règles de continuité
- On ne retire JAMAIS une entrée (marché / sélection / match) avant que le match soit
FINISHED. Un marché qui cesse d'être coté passe àstatus: "NO_QUOTE"(les dernières cotes restent).OPEN= on price activement. - État COMPLET à chaque event (pas de delta) : écrasez votre cache pour ce
fixtureId.
4) Payloads
{ "type": "odds.update", "at": "2026-07-02T13:04:12.245Z",
"fixtureId": "5546570", "bookmakerId": "betconstruct",
"markets": [ { "key": "match-winner", "name": "FT 1X2", "status": "OPEN",
"selections": [
{ "key": "home", "label": "1", "decimalOdds": 3.12, "line": null, "status": "ACTIVE" },
{ "key": "draw", "label": "X", "decimalOdds": 4.46, "line": null, "status": "ACTIVE" },
{ "key": "away", "label": "2", "decimalOdds": 2.20, "line": null, "status": "ACTIVE" } ] } ] }{ "type": "livescore.update", "at": "2026-07-02T13:47:03.500Z",
"fixtureId": "5546570", "scoreHome": 1, "scoreAway": 0,
"elapsed": 47, "period": "2H", "status": "LIVE" }{ "type": "fixture.update", "at": "2026-07-02T15:32:00.000Z",
"fixtureId": "5546570", "status": "FINISHED",
"homeScore": 2, "awayScore": 1, "startsAt": "2026-07-02T13:00:00.000Z" }Livescore : poussé à chaque changement de minute + heartbeat ~15 s en live (on continue de pousser pendant la mi-temps HT/BT avec la période figée, sans incrémenter elapsed). Pas d'event > 30 s = flux mort. On s'arrête à FINISHED. Périodes CANONIQUES : 1H HT 2H BT ET AET AP PEN FT FINISHED (foot ; non-foot inchangé).
5) Keep-alive, erreurs, reconnexion
Le serveur envoie { "type": "ping" } (~20 s) si pas d'autre trafic. Erreurs : { "type": "error", "code": "…", "message": "…" } (AUTH_EXPIRED, AUTH_INVALID, RATE_LIMITED, INTERNAL). À la reconnexion (backoff exponentiel), le snapshotre-livre l'état complet — vous ne perdez aucun match. Le rejeu par sincepourra être ajouté ultérieurement (le snapshot le couvre pour l'instant).
Limites & honnêteté des données
Pour éviter toute mauvaise surprise côté intégration, voici ce que la source (le BO BetConstruct de ce compte) n'expose pas, et que nous ne fournissons donc pas :
- Pas d'events détaillés : buts, cartons, remplacements, VAR. (Aucune route
match-events.) - Pas de compositions (lineups) ni de statistiques de match.
- Pas de settlement par sélection fourni par notre API.
Le settlement côté AGDTech se base donc sur : les scores finaux (FT), les scores mi-temps (HT, si disponibles) et les scores par période (periodScores, si disponibles).
Cadence de polling recommandée
| Domaine | Intervalle | Note |
|---|---|---|
| livescore | ~5 s (live) | matchs LIVE — notre donnée est quasi temps réel (cache ~4 s) |
| odds | ~10 s (live) · ~2 min (pregame) | live : interrogez ~10 s (cache ~12 s) ; pregame : ~2 min suffit |
| fixtures | ~30 min | catalogue stable |
Doc interactive auto-générée : https://real-odds-api-production.up.railway.app/redoc
Historique des mises à jour
Chaque modification de l'API est consignée ici. La dernière version est en tête. La date la plus récente apparaît en rouge en haut de page.
- NOUVEAU — LOGOS désormais RENSEIGNÉS quand on les a : « homeTeamLogo » / « awayTeamLogo » sur les fixtures, « logoUrl » sur GET /agd/v1/teams et GET /agd/v1/leagues, et « leagueLogo » sur les fixtures. Source : TheSportsDB (équipes) + logos de ligue internes. Absents (null, listés dans provenance.missing) pour les équipes/ligues non couvertes. Le drapeau pays (countryLogo) et l'arbitre/round restent null (non stockés).
- Rappel : on ne transmet PAS le règlement (gagné/perdu) — c'est INTERNE. Selection.status indique seulement ACTIVE / SUSPENDED / SETTLED, jamais le résultat.
- NOUVEAU — champ « venue » (lieu du match, « Stade, Ville ») désormais RENSEIGNÉ sur GET /agd/v1/fixtures/{id} ET ajouté au GET /agd/v1/livescore/{id} (optionnel, null si non couvert). Résolu via API-Football, donc disponible surtout sur les ligues connues ; absent (null) sur les petites ligues/réserves. Additif : les clients qui l'ignoraient ne cassent pas.
- NOUVEAU — WebSocket temps réel unique : wss://real-odds-api-production.up.railway.app/agd/v1/ws (Bearer). UNE seule connexion pousse TOUT (cotes, livescore, minute, statut) — plus AUCUN GET REST pour le temps réel. Snapshot COMPLET à l'abonnement (0 appel REST au boot), puis flux live. On envoie l'état COMPLET à chaque event (pas de delta), et on ne RETIRE jamais un marché (un marché fermé passe à status:"NO_QUOTE", la donnée reste). Voir la section « WebSocket temps réel ». Le SSE /stream + les GET REST restent disponibles en parallèle (migration).
- Codes période CANONIQUES sur le WebSocket : 1H, HT, 2H, BT, ET, AET, AP, PEN, FT, FINISHED (foot). Plus besoin de mapper 1p/2p/paused côté client. Non-foot (1set, 1q…) inchangé.
- Livescore WebSocket : minute (elapsed) poussée à chaque changement + heartbeat toutes les ~15 s (on continue de pousser pendant la mi-temps HT/BT avec la période figée). Absence d'event > 30 s = flux mort côté consommateur.
- RECOMMANDÉ — Passez du polling au PUSH temps réel (SSE) déjà disponible : GET /agd/v1/stream (Bearer). Au lieu de re-tirer en boucle les cotes de tous les matchs, ouvrez UNE connexion EventSource ; on vous notifie « event: update { fixtureId } » DÈS qu'un match bouge, et vous ne rafraîchissez QUE ce match (/agd/v1/odds/{id} ou /livescore/{id}). Résultat : live plus frais ET ~10× moins de requêtes (le polling massif des cotes sature inutilement la donnée).
- Option avancée (plus tard, si besoin) — Livraison directe par file (RabbitMQ, « Option B » de votre spec data) : on PUBLIE la donnée complète dans votre queue, zéro polling, durable (messages conservés si votre consommateur tombe). À activer quand vous voulez ; il nous faut juste vos accès (host, vhost, exchange/queue, credentials).
- ALIGNEMENT COMPLET des clés de marchés sur le catalogue canonique AGDTech : 272 de nos marchés mappés vers vos providerkey (vérifiés un par un contre votre catalogue de 355 clés). Sur un match football riche, ~80% des clés correspondent désormais exactement à votre catalogue (ex. match-winner, both-teams-to-score, asian-handicap, over-under-2.5, away-team-total-goals-1st-half, ht-ft-double, first-team-to-score-3-way-1st-half...).
- Marchés SANS équivalent dans votre catalogue (fenêtres minute, prolongations/penalty shootout, combos, certains marchés tennis/basket, doublebet/matchflow...) : laissés en clé vendeur (votre resolver les ignore). À voir au cas par cas si vous voulez en ajouter.
- ALIGNEMENT marchés (catalogue AGDTech) : les clés de marchés à ligne décimale utilisent désormais le POINT, pas le tiret (ex. over-under-2.5 au lieu de over-under-2-5). Idem pour toutes les lignes (handicaps, totaux).
- Renommages vers le catalogue canonique : 1st-half-1x2 → first-half-winner, 2nd-half-match-odds → second-half-winner, 1st-half-european-handicap → european-handicap-first-half, away-to-win-both-halves → away-win-both-halves, home-to-win-either-half → home-win-either-half, winner-of-the-match → match-winner. (Alignement complet en cours dès réception de la spec catalogue exhaustive.)
- FIABILITÉ — GET /agd/v1/fixtures n'advertise plus un match LIVE dont aucune cote n'est servable (tous marchés suspendus / cotes 0.00). Donc un fixture LIVE listé a TOUJOURS des cotes via /odds/{id} (plus de 404 sur un fixture listé). Fréquent sur les ligues virtuelles rapides (ex. IPBL) entre deux points ; le match réapparaît dès qu'un marché rouvre.
- CORRECTIF graphe de référence : GET /agd/v1/leagues ne renvoie plus QUE les ligues ACTIVES (≥1 match), avec un sportId VALIDE (était 'unknown' pour la plupart) et countryId quand connu. Avant, les ligues réellement utilisées par vos fixtures pouvaient être absentes de la liste (cap + ligues mortes du catalogue). Re-synchronisez /sports, /countries, /leagues pour rafraîchir votre catalogue.
- CORRECTIF : GET /agd/v1/countries renvoie désormais le référentiel pays (était vide) -> League.countryId et Fixture.countryName sont résolvables.
- Fixture.countryName est désormais renseigné (était null) en plus de leagueName/leagueId. Aucun changement de schéma : champs additifs.
- NOUVEAU — Multi-providers : GET /agd/v1/providers liste les sources disponibles. BetConstruct (roisbet) reste la source principale (tous types de paris + cote juste + settlement) via /fixtures /odds /livescore.
- NOUVEAU — Provider betPawa : GET /agd/v1/betpawa/odds renvoie les cotes betPawa en COTE JUSTE (1X2 dé-vigé, marge retirée). Une entrée par event, chaque outcome porte decimalOdds (la cote juste). Le champ matchKey (slug home/away/date) permet de RAPPROCHER l'event de vos autres sources. Filtres : ?sport=football|basketball|tennis, ?status=LIVE|UPCOMING, ?limit=. Cadence conseillée ~1-2 min.
- NOUVEAU — Comparaison multi-bookmaker : GET /agd/v1/odds/{fixtureId}/betpawa renvoie les cotes betPawa (1X2 juste) pour LE MÊME match qu'une fixture BetConstruct (que vous récupérez déjà via /odds/{id}). Rapprochement par slug home/away/date ; 404 si betPawa ne couvre pas ce match ou si les noms d'équipes diffèrent.
- NOUVEAU — PUSH temps réel (SSE) : GET /agd/v1/stream (Bearer). Ouvrez UNE connexion EventSource ; vous recevez « event: update » avec { fixtureId } DÈS qu'une cote/score bouge (poussé par notre WebSocket, latence ~secondes). À chaque event, rafraîchissez /agd/v1/odds/{id} ou /livescore/{id}. Plus besoin de poller à l'aveugle — moins de latence ET moins de requêtes.
- Caches live abaissés pour le quasi temps réel : livescore ~4 s, odds ~12 s (vous pouvez interroger plus souvent en live).
- IMPORTANT — Cotes JUSTES : decimalOdds est désormais la COTE JUSTE (marge bookmaker retirée), pas la cote brute roisbet. Formule : cote_juste = cote_roisbet × (1 + marge/100), avec la marge CONFIGURÉE par roisbet pour chaque type de pari. Chaque marché porte un champ margin (%) indiquant la marge retirée (cote_roisbet = cote_juste / (1 + margin/100)). But : vous recevez la cote réelle (probabilité vraie) et appliquez VOTRE propre marge.
- Marchés sans marge configurée : margin = null et decimalOdds = cote brute (cas rares/exotiques).
- Chaque marché porte un champ description BILINGUE { fr, en } expliquant le type de pari (le petit « i »), ou null si inconnu. Affichez description.en pour vos utilisateurs anglophones, description.fr pour les francophones.
- Scores plus fiables : homeScore/awayScore, homeHalftimeScore/awayHalftimeScore et periodScores proviennent désormais du SCORE OFFICIEL BetConstruct (bloc livescore de la page match : score final, mi-temps, scores par période), au lieu d'une reconstruction heuristique. La mi-temps est donc disponible bien plus souvent et le score est exact.
- Rattachement par identifiant : le score est joint au match par son identifiant BetConstruct (plus fiable que par nom d'équipe), réduisant tout risque d'attribution erronée.
- Statut LIVE durci (anti « faux live ») : un match dont le coup d'envoi est passé mais SANS aucun score live ET dont les cotes ne bougent plus depuis 45 min n'est plus présenté comme LIVE → il passe FINISHED. BetConstruct ne le trade plus = il n'est pas réellement en jeu.
- Garde-fou de durée par sport : foot/futsal ~2h30, basket/volley/hand/rugby ~3h, hockey 3h30, tennis 6h. Au-delà, le match est FINISHED même sans signal explicite.
- Détection de fin enrichie : la période live « ended » et le statut « off » de BetConstruct déclenchent désormais FINISHED (en plus du score final et de la fermeture de tous les marchés).
- Statuts étendus : un match peut désormais être CANCELLED (annulé/abandonné), POSTPONED (reporté) ou PAUSED (interrompu) en plus de SCHEDULED/LIVE/FINISHED — un match abandonné ne reste plus « live » indéfiniment.
- Score mi-temps : garantie anti-erreur. homeHalftimeScore/awayHalftimeScore ne sont renseignés que si la mi-temps est isolable de façon certaine ; en cas de doute → null + _provenance.missing (jamais le score final renvoyé comme mi-temps).
- Cotes : marchés over/under toujours accompagnés de leur ligne (seuil) — un over/under sans ligne n'est plus émis. Asian handicap : la clé encode le signe (ex. asian-handicap-home-minus-1-5) pour éviter toute collision entre +1.5 et -1.5.
- Cotes : déduplication des marchés par clé canonique (plus de doublon match-winner pregame/live dans le même payload).
- _provenance.warnings : signale désormais les cas ambigus (ligne introuvable, orientation de handicap incertaine, score live pas encore connu) pour une intégration en toute transparence.
- Fixture/Livescore : champ status entièrement fiabilisé. Il n'est plus calculé sur le seul coup d'envoi : un match passe FINISHED dès le score final, OU dès que tous ses marchés sont fermés, OU après 4 h (garde-fou). Un match LIVE est donc réellement en cours — fini les matchs affichés « live » des heures après la fin.
- Cohérence garantie : le filtre ?status=LIVE|FINISHED|SCHEDULED renvoie exactement les mêmes matchs que le statut affiché.
- Fixture : champs homeHalftimeScore / awayHalftimeScore renseignés quand la mi-temps est disponible (sinon null + _provenance.missing).
- Fixture : champ status fiabilisé — calculé sur le coup d'envoi (SCHEDULED avant le match, LIVE ~5h après le coup d'envoi, FINISHED au-delà).
- Livescore : periodScores rempli quand l'info de période existe (1H/2H/HT/Q1-Q4/S1-S5), + champ period courant.
- Précision : pas d'events (buts, cartons, remplacements, VAR), lineups, stats ni settlement par sélection — le settlement se base sur FT + HT + scores par période.
- Fuseaux horaires : startsAt exprimé dans le fuseau du compte (par défaut UTC+1 Cameroun) ; _provenance.fetchedAt reste en UTC.
- Version initiale : endpoints /agd/v1/{sports,countries,leagues,teams,fixtures,odds,livescore}, enveloppe _provenance, format v1, auth Bearer.