Skip to main content
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 {
		return nil, fmt.Errorf("rate limited, retry after %s", resp.Header.Get("X-RateLimit-Reset"))
	}

	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 {
	PriceValue  float64 `json:"price"`
	AffiliateID int     `json:"affiliate_id"`
}

type Line struct {
	LineValue float64          `json:"line"`
	IsMain    bool             `json:"is_main"`
	Prices    map[string]Price `json:"prices"`
}

type Participant struct {
	ParticipantID   int    `json:"participant_id"`
	Name            string `json:"name"`
	ParticipantType string `json:"participant_type,omitempty"`
	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.LineValue != 0 {
							lineStr = fmt.Sprintf(" (%g)", line.LineValue)
						}
						fmt.Printf("    %s%s: %s @ %s\n",
							p.Name, lineStr, formatPrice(price.PriceValue), 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.PriceValue), 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