> ## 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.

# Go SDK

> Use TheRundown API from Go with net/http for REST calls and gorilla/websocket for real-time streaming.

<Note>
  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.
</Note>

## Installation

No external dependencies are required for REST calls. For WebSocket support:

```bash theme={null}
go get github.com/gorilla/websocket
```

## Configuration

```go theme={null}
package main

import (
	"encoding/json"
	"errors"
	"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

```go theme={null}
func apiGet(path string, params map[string]string) ([]byte, error) {
	if apiKey == "" {
		return nil, errors.New("Set THERUNDOWN_API_KEY")
	}

	u, err := url.Parse(baseURL + path)
	if err != nil {
		return nil, err
	}

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

	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-TheRundown-Key", apiKey)

	resp, err := http.DefaultClient.Do(req)
	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

```go theme={null}
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

```go theme={null}
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)
					}
				}
			}
		}
	}
}
```

## Stats: sample first, then one completed event

The fixed sample is a zero-data-point schema check. For production stats, request one known completed event with a small `stats_ids` filter, cache the response, and fetch again only when your product needs a correction. Do not turn this into a per-team polling loop.

```go theme={null}
// Static sample: authenticate it, but do not poll it.
sample, err := apiGet("/stats/sample", nil)
if err != nil {
	return
}
fmt.Println(string(sample))

// Production: one completed MLB event and three example catalog fields only.
// Discover current IDs with GET /stats?sport_id=3; do not hard-code these IDs.
eventID := "e241d4b4967e3bc003cdc2f77666bb82"
teamBox, err := apiGet("/events/"+eventID+"/stats", map[string]string{
	"stats_ids": "271,273,276", // hits, home runs, runs
})
if err != nil {
	return
}
fmt.Println(string(teamBox))
```

The production call requires access to that event's current season. It becomes archive access after the season changes. The IDs shown are examples only: for your own request, get the event ID from the current events route and discover current supported IDs from `GET /stats?sport_id=3` before filtering. Production routes depend on effective stats access. See [Stats Access](/guides/stats-access).

## WebSocket Streaming

```go theme={null}
package main

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

	"github.com/gorilla/websocket"
)

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

type MarketPriceUpdate struct {
	EventID     string `json:"event_id"`
	MarketID    int    `json:"market_id"`
	AffiliateID int    `json:"affiliate_id"`
	Line        string `json:"line"`
	Price       string `json:"price"`
}

type WSMessage struct {
	Meta WSMeta            `json:"meta"`
	Data MarketPriceUpdate `json:"data"`
}

func handleWSMessage(msg WSMessage) {
	switch msg.Meta.Type {
	case "heartbeat":
		return
	case "market_price":
		fmt.Printf("Update: event=%s market=%d affiliate=%d line=%s price=%s\n",
			msg.Data.EventID, msg.Data.MarketID, msg.Data.AffiliateID,
			msg.Data.Line, msg.Data.Price)
	default:
		return
	}
}

func connectWebSocket() {
	if apiKey == "" {
		log.Println("Set THERUNDOWN_API_KEY")
		return
	}

	u, _ := url.Parse(wsURL)
	q := u.Query()
	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 {
		headers := http.Header{}
		headers.Set("X-TheRundown-Key", apiKey)
		conn, _, err := websocket.DefaultDialer.Dial(u.String(), headers)
		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
				}

				handleWSMessage(msg)
			}
		}()

		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

```go theme={null}
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

<CardGroup cols={2}>
  <Card title="Getting Live Odds" icon="signal" href="/guides/getting-live-odds">
    Detailed guide on fetching odds
  </Card>

  <Card title="WebSocket Streaming" icon="bolt" href="/guides/websocket-streaming">
    Real-time data streaming guide
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    All authentication methods
  </Card>

  <Card title="Rate Limits" icon="gauge" href="/rate-limits">
    Rate limit details and best practices
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.