Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.therundown.io/llms.txt

Use this file to discover all available pages before exploring further.

An official Go SDK is coming soon. In the meantime, this guide shows how to use TheRundown API directly with the standard net/http package and gorilla/websocket for WebSocket connections.

Installation

No external dependencies are required for REST calls. For WebSocket support:
go get github.com/gorilla/websocket

Configuration

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

const (
	baseURL = "https://therundown.io/api/v2"
	wsURL   = "wss://therundown.io/api/v2/ws/markets"
)

var apiKey = os.Getenv("THERUNDOWN_API_KEY")

Helper Function

func apiGet(path string, params map[string]string) ([]byte, error) {
	u, err := url.Parse(baseURL + path)
	if err != nil {
		return nil, err
	}

	q := u.Query()
	q.Set("key", apiKey)
	for k, v := range params {
		q.Set(k, v)
	}
	u.RawQuery = q.Encode()

	resp, err := http.Get(u.String())
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusTooManyRequests {
		retryAfter := resp.Header.Get("Retry-After")
		if retryAfter == "" {
			retryAfter = "60"
		}
		return nil, fmt.Errorf("rate limited, retry after %s seconds", retryAfter)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API error: %d %s", resp.StatusCode, resp.Status)
	}

	return io.ReadAll(resp.Body)
}

Getting Sports

type Sport struct {
	SportID   int    `json:"sport_id"`
	SportName string `json:"sport_name"`
}

type SportsResponse struct {
	Sports []Sport `json:"sports"`
}

func getSports() ([]Sport, error) {
	body, err := apiGet("/sports", nil)
	if err != nil {
		return nil, err
	}

	var resp SportsResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, err
	}

	return resp.Sports, nil
}

func main() {
	sports, err := getSports()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	for _, s := range sports {
		fmt.Printf("%d: %s\n", s.SportID, s.SportName)
	}
}

Getting Events with Odds

type Price struct {
	Price      float64 `json:"price"`
	IsMainLine bool    `json:"is_main_line"`
	UpdatedAt  string  `json:"updated_at"`
}

type Line struct {
	Value  string           `json:"value,omitempty"`
	Prices map[string]Price `json:"prices"`
}

type Participant struct {
	ID    int    `json:"id"`
	Type  string `json:"type"`
	Name  string `json:"name"`
	Lines []Line `json:"lines"`
}

type Market struct {
	MarketID     int           `json:"market_id"`
	Name         string        `json:"name"`
	PeriodID     int           `json:"period_id"`
	Participants []Participant `json:"participants"`
}

type Team struct {
	TeamID int    `json:"team_id"`
	Name   string `json:"name"`
}

type Event struct {
	EventID string   `json:"event_id"`
	SportID int      `json:"sport_id"`
	Teams   []Team   `json:"teams"`
	Markets []Market `json:"markets"`
}

type EventsResponse struct {
	Events []Event `json:"events"`
}

func getEvents(sportID int, date string) ([]Event, error) {
	path := fmt.Sprintf("/sports/%d/events/%s", sportID, date)
	body, err := apiGet(path, map[string]string{
		"market_ids":    "1,2,3",
		"affiliate_ids": "19,23",
		"main_line":     "true",
	})
	if err != nil {
		return nil, err
	}

	var resp EventsResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, err
	}

	return resp.Events, nil
}

func formatPrice(p float64) string {
	if p == 0.0001 {
		return "N/A"
	}
	if p > 0 {
		return fmt.Sprintf("+%d", int(p))
	}
	return fmt.Sprintf("%d", int(p))
}

func main() {
	today := time.Now().Format("2006-01-02")
	events, err := getEvents(4, today)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	for _, event := range events {
		away := event.Teams[0].Name
		home := event.Teams[1].Name
		fmt.Printf("\n%s @ %s\n", away, home)

		for _, market := range event.Markets {
			fmt.Printf("  %s:\n", market.Name)
			for _, p := range market.Participants {
				for _, line := range p.Lines {
					for affID, price := range line.Prices {
						lineStr := ""
						if line.Value != "" {
							lineStr = fmt.Sprintf(" (%s)", line.Value)
						}
						fmt.Printf("    %s%s: %s @ %s\n",
							p.Name, lineStr, formatPrice(price.Price), affID)
					}
				}
			}
		}
	}
}

WebSocket Streaming

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"math"
	"math/rand"
	"net/url"
	"os"
	"os/signal"
	"time"

	"github.com/gorilla/websocket"
)

type WSMeta struct {
	Type      string `json:"type"`
	Timestamp string `json:"timestamp"`
}

type WSMessage struct {
	EventID    string        `json:"event_id"`
	SportID    int           `json:"sport_id"`
	MarketID   int           `json:"market_id"`
	MarketName string        `json:"market_name"`
	Participants []Participant `json:"participants"`
	Meta       WSMeta        `json:"meta"`
}

func connectWebSocket() {
	u, _ := url.Parse(wsURL)
	q := u.Query()
	q.Set("key", apiKey)
	q.Set("sport_ids", "4")
	q.Set("market_ids", "1,2,3")
	u.RawQuery = q.Encode()

	interrupt := make(chan os.Signal, 1)
	signal.Notify(interrupt, os.Interrupt)

	reconnectDelay := 1.0
	maxDelay := 30.0

	for {
		conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
		if err != nil {
			jitter := rand.Float64()
			delay := math.Min(reconnectDelay+jitter, maxDelay)
			log.Printf("Connection failed: %v. Retrying in %.1fs...", err, delay)
			time.Sleep(time.Duration(delay * float64(time.Second)))
			reconnectDelay = math.Min(reconnectDelay*2, maxDelay)
			continue
		}

		log.Println("WebSocket connected")
		reconnectDelay = 1.0

		done := make(chan struct{})

		go func() {
			defer close(done)
			for {
				_, message, err := conn.ReadMessage()
				if err != nil {
					log.Printf("Read error: %v", err)
					return
				}

				var msg WSMessage
				if err := json.Unmarshal(message, &msg); err != nil {
					continue
				}

				if msg.Meta.Type == "heartbeat" {
					continue
				}

				fmt.Printf("Update: %s - %s\n", msg.EventID, msg.MarketName)
				for _, p := range msg.Participants {
					for _, line := range p.Lines {
						for affID, price := range line.Prices {
							fmt.Printf("  %s: %s @ %s\n",
								p.Name, formatPrice(price.Price), affID)
						}
					}
				}
			}
		}()

		select {
		case <-done:
			log.Println("Connection lost, reconnecting...")
			conn.Close()
		case <-interrupt:
			log.Println("Shutting down...")
			conn.WriteMessage(
				websocket.CloseMessage,
				websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
			)
			conn.Close()
			return
		}
	}
}

func main() {
	connectWebSocket()
}

Error Handling with Retry

func apiGetWithRetry(path string, params map[string]string, maxRetries int) ([]byte, error) {
	for attempt := 0; attempt < maxRetries; attempt++ {
		body, err := apiGet(path, params)
		if err == nil {
			return body, nil
		}

		// Check if rate limited
		if err.Error()[:12] == "rate limited" {
			wait := time.Duration(math.Pow(2, float64(attempt))) * time.Second
			jitter := time.Duration(rand.Intn(1000)) * time.Millisecond
			log.Printf("Rate limited. Retrying in %v...", wait+jitter)
			time.Sleep(wait + jitter)
			continue
		}

		return nil, err
	}

	return nil, fmt.Errorf("max retries exceeded")
}

Next Steps

Getting Live Odds

Detailed guide on fetching odds

WebSocket Streaming

Real-time data streaming guide

Authentication

All authentication methods

Rate Limits

Rate limit details and best practices