goalapi

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 25 Imported by: 0

README

goal-go

Go client for the GOAL API: football fixtures, live scores, standings, player stats, odds and real-time match updates over WebSocket.

Standard library only, no dependencies — including the WebSocket client. Go 1.21+.

go get github.com/goal-api/goal-api-go

Quick start

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    goalapi "github.com/goal-api/goal-api-go"
)

type Fixture struct {
    HomeTeam  struct{ Name string } `json:"homeTeam"`
    AwayTeam  struct{ Name string } `json:"awayTeam"`
    HomeScore *int                  `json:"homeScore"`
    AwayScore *int                  `json:"awayScore"`
}

func main() {
    client, err := goalapi.New(os.Getenv("GOAL_API_KEY"))
    if err != nil {
        log.Fatal(err)
    }

    page, err := client.Fixtures.Live(context.Background(), nil)
    if err != nil {
        log.Fatal(err)
    }

    var live []Fixture
    if err := page.Into(&live); err != nil {
        log.Fatal(err)
    }
    for _, match := range live {
        fmt.Println(match.HomeTeam.Name, "vs", match.AwayTeam.Name)
    }
}

Get a key at goal-api.com/signup.

A *Client is safe for concurrent use. Share one so connections pool and the rate-limit snapshot stays meaningful.

Client options

client, err := goalapi.New(apiKey,
    goalapi.WithTimeout(15*time.Second),   // per attempt; default 30s
    goalapi.WithMaxRetries(3),             // 429 + 5xx + network errors; default 2
    goalapi.WithBaseURL("https://api.goal-api.com/v1"),
    goalapi.WithHTTPClient(myInstrumentedClient),
    goalapi.WithHeader("X-My-App", "scoreboard"),
)

Retries use exponential backoff with full jitter and always honour a server-sent Retry-After. A cancelled context is never retried.

Responses

Page and Item keep Data as json.RawMessage, since rows are provider-shaped and change. Decode into your own types:

page, err := client.Leagues.Standings(ctx, leagueID, nil)

type Row struct {
    Position int    `json:"position"`
    Points   int    `json:"points"`
    Team     struct{ Name string } `json:"team"`
}
var table []Row
if err := page.Into(&table); err != nil { ... }

page.Pagination.HasMore   // *Pagination, nil on single-resource endpoints
page.Source               // "cache" | "database"

For scripts, page.Rows() and item.Row() give you map[string]any without a struct.

Endpoints

Grouped by resource. Params is a map[string]any; nil and empty values are dropped, so build it unconditionally. Full reference in ENDPOINTS.md.

client.Status.Get(ctx)                                  // no API key needed
client.Countries.List(ctx, goalapi.Params{"search": "spa"})
client.Leagues.List(ctx, goalapi.Params{"isActive": true, "limit": 100})
client.Leagues.Standings(ctx, leagueID, nil)
client.Leagues.TopScorers(ctx, leagueID, goalapi.Params{"limit": 10})
client.Teams.Get(ctx, teamID, goalapi.Params{"includePlayers": true})
client.Teams.Statistics(ctx, teamID, goalapi.Params{"season": "2025-2026"})
client.Fixtures.List(ctx, goalapi.Params{"from": "2026-08-01", "to": "2026-08-07", "status": goalapi.StatusScheduled})
client.Fixtures.ByDate(ctx, "2026-08-15", goalapi.Params{"leagueId": leagueID})
client.Fixtures.Lineups(ctx, fixtureID)
client.Fixtures.Statistics(ctx, fixtureID, goalapi.Params{"half": goalapi.HalfFirst})
client.Standings.Form(ctx, leagueID, nil)
client.Players.Search(ctx, "haaland", goalapi.Params{"limit": 5})
client.Players.Compare(ctx, playerA, playerB)
client.Players.Top(ctx, goalapi.StatGoals, goalapi.Params{"limit": 20})
client.Coaches.ByTeam(ctx, teamID)
client.H2H.Stats(ctx, teamA, teamB)
client.Results.Today(ctx)
client.Videos.Recent(ctx, goalapi.Params{"leagueId": leagueID, "limit": 10})
client.Odds.List(ctx, goalapi.Params{"bookmaker": "bet365"})
client.Predictions.List(ctx, goalapi.Params{"matchId": matchID})

Enum values are constants: Status*, PlayerType*, Stat*, Half*.

One exception: client.Status.*

The five /public/* endpoints don't use the {success, data} envelope. They return bare objects, so these methods hand back Raw rather than Page or Item:

body, err := client.Status.Get(ctx)

var status struct {
    Status     string `json:"status"`
    Components []struct{ Name, Status string } `json:"components"`
}
if err := body.Into(&status); err != nil { ... }

They also paginate with page/limit instead of limit/offset, so Paginate does not apply to CoverageLeagues.

Pagination

fetch := func(ctx context.Context, p goalapi.Params) (*goalapi.Page, error) {
    return client.Leagues.Teams(ctx, leagueID, p)
}

// Collect everything into a slice:
var teams []Team
if err := client.CollectInto(ctx, fetch, nil, &teams); err != nil { ... }

// Or stream, to avoid holding it all in memory:
pager := client.Paginate(fetch, &goalapi.PaginateOptions{PageSize: 500, MaxItems: 5000})
for pager.Next(ctx) {
    var team Team
    if err := pager.Into(&team); err != nil { ... }
    fmt.Println(team.Name)
}
if err := pager.Err(); err != nil { ... }

Default PageSize is 100, the limit ceiling on most endpoints. /results and /countries take 500.

Errors

One *Error type plus sentinels, which is the Go-idiomatic shape. Branch with errors.Is, read the detail with errors.As:

page, err := client.Fixtures.Get(ctx, fixtureID)
switch {
case err == nil:
    // ...
case errors.Is(err, goalapi.ErrNotFound):
    return nil, nil
case errors.Is(err, goalapi.ErrRateLimited):
    var apiErr *goalapi.Error
    errors.As(err, &apiErr)
    time.Sleep(time.Duration(apiErr.RetryAfter) * time.Second)
case errors.Is(err, goalapi.ErrValidation):
    var apiErr *goalapi.Error
    errors.As(err, &apiErr)
    log.Printf("server rejected: %s %s", apiErr.Message, apiErr.Details)
default:
    return nil, err
}

Sentinels: ErrValidation, ErrAuthentication, ErrPermission, ErrPlanUpgradeRequired, ErrNotFound, ErrConflict, ErrRateLimited, ErrServiceUnavailable, ErrServer, ErrTimeout, ErrNetwork.

Two error shapes

The API answers with one of two bodies, and the SDK normalises both:

Gateway (auth, routing, rate limits) Football service (most endpoints)
text message error
code yes yes
category yes no
correlationId yes no
details object array, on validation errors

So apiErr.Message and apiErr.Code are always populated, and apiErr.CorrelationID is only set on gateway errors. Quote it in a support ticket when you have it.

Rate limits
quota := client.RateLimit()
// quota.Limit, quota.Remaining, quota.Reset (unix seconds), quota.Type ("DAILY"|"MONTHLY")

Live WebSocket updates

The socket is at wss://api.goal-api.com/ws, not /v1/ws. Only nginx's location ^~ /ws carries the Upgrade headers; /v1/ws is proxied as ordinary HTTP and answers 200 instead of upgrading. The SDK derives the right URL for you.

Two services authenticate: the gateway authorises the upgrade from the header or ?wsToken=, then websocket-service needs an {"type": "auth", ...} frame as the very first message. The SDK sends it, and treats auth_success as the point the connection is usable.

subscribe is capped per plan and the cap can be 0. auth_success reports maxSubscriptions; if it is 0 the socket works but no match_update will ever arrive. See the known server issue in ENDPOINTS.md.

client.Live() is the client. It owns the socket, the auth handshake, keepalives and reconnection, and it still pulls in no dependencies: the RFC 6455 client is implemented on the standard library in ws.go.

live := client.Live()

live.On(goalapi.LiveMatchUpdate, func(msg goalapi.LiveMessage) {
    var update MatchUpdate
    _ = msg.Into(&update)
})

if err := live.Connect(ctx); err != nil {   // returns once auth_success arrives
    return err
}
defer live.Close()

live.Subscribe(fixtureID)
live.Run(ctx)                               // blocks until ctx ends or it gives up

Or skip handlers and range over the channel, which is usually the more Go-shaped way:

for msg := range live.Messages() {
    if msg.Type == goalapi.LiveMatchUpdate {
        var update MatchUpdate
        _ = msg.Into(&update)
    }
}

Handlers run on the reader goroutine, one message at a time, so a handler that blocks stops the feed; hand slow work to a goroutine of your own. Messages() is a buffered channel (1000 by default) that drops the oldest message when a consumer falls behind, so a slow reader costs history rather than the connection.

Subscriptions are replayed after a reconnect, because the server does not remember a dropped connection's. Options: WithLiveAutoReconnect, WithLiveMaxReconnectAttempts, WithLivePingInterval, WithLiveReadTimeout, WithLiveQueueSize, WithLiveAuthTimeout, WithLiveTLSConfig, WithLiveConnectToken, WithLiveURL.

Events: the server types below, plus LiveEventOpen and LiveEventClose for the transport itself, and LiveEventAny for everything.

Driving the socket yourself

The frame builders stay exported, for when you want the protocol without the connection management — an existing event loop, or a WebSocket library you already depend on: SubscribeMessage, UnsubscribeMessage, PingMessage, StatusMessage, ListSubscriptionsMessage, alongside WebSocketURL(), WebSocketHeader() and AuthMessage(). Server message types: LiveMatchUpdate, LiveAuthSuccess, LiveStatus, LivePong, LiveServerShutdown, LiveError, LiveSubscribeResponse, LiveUnsubscribeResponse, LiveGetSubscriptionsResponse.

The server caps client messages at 60/minute and concurrent subscriptions by plan. Only resource: "match" is supported.

Handing a token to a browser

A Go server can set the Authorization header, so it needs no token. Mint one when your backend hands live access to a frontend, so the browser never sees your API key:

token, err := client.MintConnectToken(ctx)
// browser: new WebSocket(`wss://api.goal-api.com/v1/ws?wsToken=${token.Token}`)

Single-use, consumed on first connect.

Webhooks

Verify against the raw body. A decoded-and-re-encoded struct has different bytes and will never match.

func handler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
    if err != nil {
        http.Error(w, "read failed", http.StatusBadRequest)
        return
    }

    raw, err := goalapi.VerifyWebhook(body, r.Header.Get(goalapi.SignatureHeader), secret, 0)
    if err != nil {
        http.Error(w, "bad signature", http.StatusBadRequest)
        return
    }

    switch r.Header.Get(goalapi.EventHeader) {
    case goalapi.EventGoalScored:
        var event GoalScored
        _ = json.Unmarshal(raw, &event)
    case goalapi.EventMatchFinished:
        // ...
    }
    w.WriteHeader(http.StatusOK)   // ack fast; retries are ~1m, 5m, 25m, 2h, 10h
}

tolerance of 0 uses DefaultWebhookTolerance (5 minutes). Deliveries older than that are rejected as replays.

Escape hatch

For an endpoint this SDK doesn't wrap yet:

var page goalapi.Page
err := client.Get(ctx, "/some/new/endpoint", goalapi.Params{"limit": 10}, &page)

Examples

Directory Shows
examples/basic Status, live fixtures, standings, pagination
examples/live The live socket: subscribing and handling frames
examples/export Walking every page of a collection to CSV
GOAL_API_KEY=... go run ./examples/basic
GOAL_API_KEY=... go run ./examples/live
GOAL_API_KEY=... go run ./examples/export > countries.csv

Testing

go test -race ./...                       # unit tests, no network
GOAL_API_KEY=... go test -race ./...      # also runs the live tests
go vet ./... && gofmt -l .
go run ./examples/basic

The live tests skip themselves without a key. Endpoint-by-endpoint coverage of the API lives in tools/sweep.py in the SDK workspace.

Licence

MIT. See LICENSE.

No dependencies, direct or transitive: standard library only, which is why there is no go.sum. See THIRD_PARTY_NOTICES.md.

Security issues: SECURITY.md.

Documentation

Overview

Package goalapi is the Go client for the GOAL API: football fixtures, live scores, standings, player stats and odds. Standard library only.

https://goal-api.com/documentation

client, err := goalapi.New(os.Getenv("GOAL_API_KEY"))
if err != nil {
    log.Fatal(err)
}
page, err := client.Fixtures.Live(ctx, nil)

Index

Constants

View Source
const (
	// DefaultBaseURL is the production GOAL API endpoint.
	DefaultBaseURL = "https://api.goal-api.com/v1"
	// Version of this SDK, sent as part of the User-Agent.
	Version = "1.0.0"
)
View Source
const (
	LiveAuthSuccess              = "auth_success"
	LiveMatchUpdate              = "match_update"
	LivePong                     = "pong"
	LiveStatus                   = "status"
	LiveServerShutdown           = "server_shutdown"
	LiveError                    = "error"
	LiveSubscribeResponse        = "subscribe_response"
	LiveUnsubscribeResponse      = "unsubscribe_response"
	LiveGetSubscriptionsResponse = "get_subscriptions_response"
)

Server to client message types. Replies to client requests are named "<request>_response", so subscribe is answered with subscribe_response.

View Source
const (
	LiveEventOpen  = "open"
	LiveEventClose = "close"
	// LiveEventAny receives every message, whatever its type.
	LiveEventAny = "*"
)

LiveEventOpen and LiveEventClose are emitted by the SDK rather than the server, so a caller can react to the transport itself. They are delivered to On handlers and to Messages like any other event.

View Source
const (
	StatusScheduled = "SCHEDULED"
	StatusLive      = "LIVE"
	StatusFinished  = "FINISHED"
	StatusHalfTime  = "HALF_TIME"
	StatusAfterET   = "AFTER_ET"
	StatusAfterPen  = "AFTER_PEN"
	StatusPostponed = "POSTPONED"
	StatusCancelled = "CANCELLED"
	StatusAwarded   = "AWARDED"
	StatusAbandoned = "ABANDONED"
	StatusSuspended = "SUSPENDED"
)

Values accepted by the "status" query param.

View Source
const (
	PlayerTypeGoalkeepers = "Goalkeepers"
	PlayerTypeDefenders   = "Defenders"
	PlayerTypeMidfielders = "Midfielders"
	PlayerTypeForwards    = "Forwards"
)

Values accepted by the "type" query param on player endpoints.

View Source
const (
	StatGoals         = "goals"
	StatAssists       = "assists"
	StatYellowCards   = "yellowCards"
	StatRedCards      = "redCards"
	StatRating        = "rating"
	StatMatchPlayed   = "matchPlayed"
	StatMinutes       = "minutes"
	StatSaves         = "saves"
	StatTackles       = "tackles"
	StatShotsTotal    = "shotsTotal"
	StatKeyPasses     = "keyPasses"
	StatPasses        = "passes"
	StatInterceptions = "interceptions"
	StatDuelsWon      = "duelsWon"
	StatDribbleSucc   = "dribbleSucc"
)

Values accepted as the stat path segment of /players/top/{stat}.

View Source
const (
	HalfFull   = "full"
	HalfFirst  = "1half"
	HalfSecond = "2half"
)

Values accepted by the "half" query param on fixture statistics.

View Source
const (
	EventMatchStarted       = "match.started"
	EventMatchFinished      = "match.finished"
	EventGoalScored         = "goal.scored"
	EventScoreChanged       = "score.changed"
	EventMatchStatusChanged = "match.status_changed"
)

Webhook event names.

View Source
const (
	SignatureHeader = "X-Goal-Signature"
	EventHeader     = "X-Goal-Event"
	DeliveryHeader  = "X-Goal-Delivery"
)

Headers on an inbound webhook delivery.

View Source
const DefaultWebhookTolerance = 5 * time.Minute

DefaultWebhookTolerance is how much clock skew / delivery latency is accepted before a delivery is treated as a replay.

Variables

View Source
var (
	ErrValidation          = sentinel("validation failed")
	ErrAuthentication      = sentinel("authentication failed")
	ErrPermission          = sentinel("access denied")
	ErrPlanUpgradeRequired = sentinel("plan upgrade required")
	ErrNotFound            = sentinel("not found")
	ErrConflict            = sentinel("conflict")
	ErrRateLimited         = sentinel("rate limited")
	ErrServiceUnavailable  = sentinel("service unavailable")
	ErrServer              = sentinel("server error")
	ErrTimeout             = sentinel("request timed out")
	ErrNetwork             = sentinel("network error")
)

Sentinels for errors.Is. See Error.Is for the mapping.

View Source
var ErrWebhookSignature = errors.New("goalapi: webhook signature verification failed")

ErrWebhookSignature wraps every verification failure, so callers can branch with errors.Is(err, goalapi.ErrWebhookSignature) and answer 400.

MatchStatuses lists every value the "status" query param accepts.

PlayerStats lists every value /players/top/{stat} accepts.

WebhookEvents lists every event a webhook endpoint can subscribe to.

Functions

func AuthMessageWithToken

func AuthMessageWithToken(token string) map[string]any

AuthMessageWithToken is the browser-side variant, using a token from MintConnectToken rather than the raw API key.

func CSV

func CSV(values ...string) string

CSV joins ids for the /players/compare endpoint.

func ListSubscriptionsMessage

func ListSubscriptionsMessage() map[string]any

ListSubscriptionsMessage asks the server which matches this connection is subscribed to.

func PingMessage

func PingMessage() map[string]any

PingMessage builds a keepalive frame. The server replies with a "pong".

func StatusMessage

func StatusMessage() map[string]any

StatusMessage asks for the connection's plan, subscriptions and feature flags.

func SubscribeMessage

func SubscribeMessage(matchID string) map[string]any

SubscribeMessage builds the frame that subscribes to a match.

func UnsubscribeMessage

func UnsubscribeMessage(matchID string) map[string]any

UnsubscribeMessage builds the frame that unsubscribes from a match.

func VerifyWebhook

func VerifyWebhook(payload []byte, signatureHeader, secret string, tolerance time.Duration) (json.RawMessage, error)

VerifyWebhook verifies an inbound webhook and returns the raw JSON body.

payload MUST be the exact request bytes, read before any JSON decoding. Re-encoding a decoded struct reorders keys and changes whitespace, which changes the HMAC and fails every time:

func handler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
    raw, err := goalapi.VerifyWebhook(body, r.Header.Get(goalapi.SignatureHeader), secret, 0)
    if err != nil {
        http.Error(w, "bad signature", http.StatusBadRequest)
        return
    }
    // r.Header.Get(goalapi.EventHeader) tells you which event this is
}

tolerance of 0 uses DefaultWebhookTolerance. Negative disables the timestamp check, which is only sensible if you dedupe on X-Goal-Delivery yourself.

Types

type Client

type Client struct {

	// Resource groups.
	Status      *StatusService
	Countries   *CountriesService
	Leagues     *LeaguesService
	Teams       *TeamsService
	Fixtures    *FixturesService
	Standings   *StandingsService
	Players     *PlayersService
	Coaches     *CoachesService
	H2H         *H2HService
	Results     *ResultsService
	Videos      *VideosService
	Odds        *OddsService
	Predictions *PredictionsService
	// contains filtered or unexported fields
}

Client is a GOAL API client, safe for concurrent use. Share one: the http.Client pools connections and the rate-limit snapshot is per-client.

func New

func New(apiKey string, opts ...Option) (*Client, error)

New creates a client. Get an API key at https://goal-api.com/dashboard.

func (*Client) AuthMessage

func (c *Client) AuthMessage() map[string]any

AuthMessage builds the frame that must be sent first on a new connection. Anything else first and the server closes with 4001.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the endpoint this client talks to.

func (*Client) CollectInto

func (c *Client) CollectInto(ctx context.Context, fetch PageFunc, opts *PaginateOptions, dst any) error

CollectInto walks every page and decodes all rows into dst, a pointer to a slice.

var teams []Team
err := client.CollectInto(ctx, fetch, nil, &teams)

func (*Client) CollectRows

func (c *Client) CollectRows(ctx context.Context, fetch PageFunc, opts *PaginateOptions) ([]map[string]any, error)

CollectRows walks every page and returns generic maps. Fine for scripts; use CollectInto elsewhere.

func (*Client) Get

func (c *Client) Get(ctx context.Context, path string, params Params, out any) error

Get decodes a raw GET into out, for endpoints not wrapped here yet. Prefer the service methods. out may be *Page, *Item, *Raw or your own type.

func (*Client) Live

func (c *Client) Live(opts ...LiveOption) *LiveClient

Live creates a live feed client. Nothing connects until Connect is called.

func (*Client) MintConnectToken

func (c *Client) MintConnectToken(ctx context.Context) (*ConnectToken, error)

MintConnectToken mints a token for a browser client.

Go servers can set the Authorization header, so they don't need this. Use it when your backend hands a token to a frontend, so the browser can connect to wss://.../ws?wsToken=<token> without seeing your API key. Single-use, consumed on first connect.

func (*Client) Paginate

func (c *Client) Paginate(fetch PageFunc, opts *PaginateOptions) *Paginator

Paginate builds a Paginator. opts may be nil for the defaults.

func (*Client) Post

func (c *Client) Post(ctx context.Context, path string, body any, out any) error

Post performs a raw POST. Not retried: /ws/token is single-use, so a retry would burn the token the first attempt may already have minted.

func (*Client) RateLimit

func (c *Client) RateLimit() RateLimit

RateLimit returns quota from the last response. Zero until the first authenticated call.

func (*Client) WebSocketHeader

func (c *Client) WebSocketHeader() http.Header

WebSocketHeader returns the headers to send on the handshake. Pass it to your WebSocket library's dial options.

func (*Client) WebSocketURL

func (c *Client) WebSocketURL() string

WebSocketURL returns the live endpoint, derived from the client's base URL so a staging override carries over.

Note the path is /ws on the host root, not /v1/ws. nginx routes the socket with "location ^~ /ws", the only location carrying the Upgrade headers; /v1/ws falls into the REST location and silently answers 200 rather than upgrading.

type CoachesService

type CoachesService struct {
	// contains filtered or unexported fields
}

CoachesService covers /coaches.

func (*CoachesService) ByCountry

func (s *CoachesService) ByCountry(ctx context.Context, country string, params Params) (*Page, error)

func (*CoachesService) ByTeam

func (s *CoachesService) ByTeam(ctx context.Context, teamID string) (*Page, error)

func (*CoachesService) Get

func (s *CoachesService) Get(ctx context.Context, coachID string) (*Item, error)

func (*CoachesService) List

func (s *CoachesService) List(ctx context.Context, params Params) (*Page, error)

func (*CoachesService) Search

func (s *CoachesService) Search(ctx context.Context, query string, params Params) (*Page, error)

type ConnectToken

type ConnectToken struct {
	Token     string `json:"token"`
	ExpiresIn int    `json:"expiresIn"`
}

ConnectToken is a short-lived, single-use WebSocket handshake token.

type CountriesService

type CountriesService struct {
	// contains filtered or unexported fields
}

CountriesService covers /countries.

func (*CountriesService) Get

func (s *CountriesService) Get(ctx context.Context, countryID string) (*Item, error)

func (*CountriesService) Leagues

func (s *CountriesService) Leagues(ctx context.Context, countryID string, params Params) (*Page, error)

func (*CountriesService) List

func (s *CountriesService) List(ctx context.Context, params Params) (*Page, error)

type Error

type Error struct {
	// StatusCode is the HTTP status, or 0 for a network/timeout failure.
	StatusCode int
	// Message comes from the response body, or from the SDK on a transport failure.
	Message string
	// Code is the API's machine-readable code, e.g. "VALIDATION_ERROR".
	Code string
	// Category groups the code, e.g. "validation", "not_found".
	Category string
	// Details is per-field validation info: an object from the gateway, an array from
	// football-service.
	Details json.RawMessage
	// CorrelationID is set on gateway errors only, not on football-service ones.
	CorrelationID string

	// Timeout is true when the request exceeded the client timeout.
	Timeout bool
	// Network is true when no HTTP response was produced at all.
	Network bool

	// RetryAfter is the server-requested wait in seconds, set on 429.
	RetryAfter int
	// Limit, Remaining, Reset and RateLimitType mirror the X-RateLimit-* headers on a 429.
	Limit         int
	Remaining     int
	Reset         int64
	RateLimitType string
	// contains filtered or unexported fields
}

Error is the only error type this SDK returns. Branch on the class of failure with errors.Is against the sentinels below, and get at the fields with errors.As:

var apiErr *goalapi.Error
if errors.As(err, &apiErr) {
    log.Printf("%s (correlation %s)", apiErr.Code, apiErr.CorrelationID)
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is maps the sentinels below onto status codes.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type FixturesService

type FixturesService struct {
	// contains filtered or unexported fields
}

FixturesService covers /fixtures.

func (*FixturesService) ByDate

func (s *FixturesService) ByDate(ctx context.Context, date string, params Params) (*Page, error)

ByDate takes a YYYY-MM-DD date.

func (*FixturesService) Cards

func (s *FixturesService) Cards(ctx context.Context, fixtureID string) (*Page, error)

func (*FixturesService) Commentary

func (s *FixturesService) Commentary(ctx context.Context, fixtureID string) (*Page, error)

func (*FixturesService) Events

func (s *FixturesService) Events(ctx context.Context, fixtureID string) (*Page, error)

func (*FixturesService) Get

func (s *FixturesService) Get(ctx context.Context, fixtureID string) (*Item, error)

func (*FixturesService) Lineups

func (s *FixturesService) Lineups(ctx context.Context, fixtureID string) (*Item, error)

func (*FixturesService) List

func (s *FixturesService) List(ctx context.Context, params Params) (*Page, error)

func (*FixturesService) Live

func (s *FixturesService) Live(ctx context.Context, params Params) (*Page, error)

Live returns matches in play. Takes an optional "leagueId".

func (*FixturesService) LiveOdds

func (s *FixturesService) LiveOdds(ctx context.Context, fixtureID string) (*Page, error)

func (*FixturesService) Odds

func (s *FixturesService) Odds(ctx context.Context, fixtureID string) (*Page, error)

Odds accepts a matchApiId as well as a fixture id, as do the three below.

func (*FixturesService) Predictions

func (s *FixturesService) Predictions(ctx context.Context, fixtureID string) (*Item, error)

func (*FixturesService) Statistics

func (s *FixturesService) Statistics(ctx context.Context, fixtureID string, params Params) (*Item, error)

Statistics takes an optional "half": HalfFull, HalfFirst or HalfSecond.

func (*FixturesService) Substitutions

func (s *FixturesService) Substitutions(ctx context.Context, fixtureID string) (*Page, error)

type H2HService

type H2HService struct {
	// contains filtered or unexported fields
}

H2HService covers /h2h. The two ids must differ, and 404 means the teams have never met.

func (*H2HService) Direct

func (s *H2HService) Direct(ctx context.Context, team1ID, team2ID string, params Params) (*Page, error)

func (*H2HService) Get

func (s *H2HService) Get(ctx context.Context, team1ID, team2ID string) (*Item, error)

func (*H2HService) Stats

func (s *H2HService) Stats(ctx context.Context, team1ID, team2ID string) (*Item, error)

type Item

type Item struct {
	Success bool            `json:"success"`
	Data    json.RawMessage `json:"data"`
	Source  string          `json:"source,omitempty"`

	FixtureID  string `json:"fixtureId,omitempty"`
	MatchAPIID string `json:"matchApiId,omitempty"`
}

Item is a single-resource response.

func (*Item) Into

func (i *Item) Into(dst any) error

Into decodes the Data object into dst, a pointer to your struct.

func (*Item) Row

func (i *Item) Row() (map[string]any, error)

Row decodes Data as a generic map.

type LeaguesService

type LeaguesService struct {
	// contains filtered or unexported fields
}

LeaguesService covers /leagues.

func (*LeaguesService) Fixtures

func (s *LeaguesService) Fixtures(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*LeaguesService) Get

func (s *LeaguesService) Get(ctx context.Context, leagueID string) (*Item, error)

func (*LeaguesService) List

func (s *LeaguesService) List(ctx context.Context, params Params) (*Page, error)

func (*LeaguesService) Results

func (s *LeaguesService) Results(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*LeaguesService) Standings

func (s *LeaguesService) Standings(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*LeaguesService) Teams

func (s *LeaguesService) Teams(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*LeaguesService) TopScorers

func (s *LeaguesService) TopScorers(ctx context.Context, leagueID string, params Params) (*Page, error)

type LiveClient

type LiveClient struct {
	// contains filtered or unexported fields
}

LiveClient is a connection to the live match feed.

It owns the socket, the auth handshake, keepalives and reconnection. Create one with Client.Live, register handlers, then Connect:

live := client.Live()
live.On(goalapi.LiveMatchUpdate, func(m goalapi.LiveMessage) {
    var match Match
    _ = m.Into(&match)
})
if err := live.Connect(ctx); err != nil {
    return err
}
defer live.Close()
live.Subscribe(fixtureID)
live.Run(ctx)

Or skip handlers entirely and range over Messages, which is usually the more Go-shaped way to write it:

for msg := range live.Messages() {
    if msg.Type == goalapi.LiveMatchUpdate {
        // ...
    }
}

Handlers run on the reader goroutine, one message at a time and in registration order. A handler that blocks stops the feed, so hand slow work to a goroutine of your own.

func (*LiveClient) Close

func (l *LiveClient) Close() error

Close shuts the connection down and stops reconnecting. Safe to call more than once.

func (*LiveClient) Connect

func (l *LiveClient) Connect(ctx context.Context) error

Connect dials, authenticates and starts the reader.

It returns once the server has accepted the auth frame, so a caller can Subscribe immediately afterwards. The ctx bounds the connect only; use Close or Run's ctx to stop the feed later.

func (*LiveClient) Connected

func (l *LiveClient) Connected() bool

Connected reports whether the socket is up and authenticated.

func (*LiveClient) ListSubscriptions

func (l *LiveClient) ListSubscriptions() error

ListSubscriptions asks the server what this connection is subscribed to. The answer arrives as a LiveGetSubscriptionsResponse message.

func (*LiveClient) Messages

func (l *LiveClient) Messages() <-chan LiveMessage

Messages returns the message stream. It is closed when the client stops for good, so it is safe to range over.

func (*LiveClient) On

func (l *LiveClient) On(event string, handler func(LiveMessage)) func()

On registers a handler and returns a function that removes it.

Event is a server message type such as LiveMatchUpdate, one of LiveEventOpen or LiveEventClose, or LiveEventAny for everything.

func (*LiveClient) Ping

func (l *LiveClient) Ping() error

Ping sends a keepalive. The server answers with LivePong. The ping loop already does this on a timer.

func (*LiveClient) RequestStatus

func (l *LiveClient) RequestStatus() error

RequestStatus asks for the connection's plan, subscriptions and feature flags. The answer arrives as a LiveStatus message.

func (*LiveClient) Run

func (l *LiveClient) Run(ctx context.Context) error

Run blocks until the client stops, ctx is cancelled, or reconnection gives up.

It returns nil for a clean shutdown, ctx.Err() on cancellation, and the underlying failure otherwise.

func (*LiveClient) Subscribe

func (l *LiveClient) Subscribe(matchID string) error

Subscribe asks for updates on a match.

The id is recorded before the frame goes out, so it survives a reconnect even if the write fails. The server caps concurrent subscriptions by plan and client messages at 60/minute.

func (*LiveClient) Subscriptions

func (l *LiveClient) Subscriptions() []string

Subscriptions lists the matches this connection is subscribed to, sorted. They are replayed automatically after a reconnect.

func (*LiveClient) Unsubscribe

func (l *LiveClient) Unsubscribe(matchID string) error

Unsubscribe stops updates for a match.

type LiveMessage

type LiveMessage struct {
	Type      string          `json:"type"`
	Data      json.RawMessage `json:"data,omitempty"`
	Timestamp int64           `json:"timestamp,omitempty"`
	Success   *bool           `json:"success,omitempty"`

	// Set on an "error" frame.
	Message string `json:"message,omitempty"`
	Code    string `json:"code,omitempty"`
}

LiveMessage is a frame from the live WebSocket.

func (*LiveMessage) Into

func (m *LiveMessage) Into(dst any) error

Into decodes the Data payload into dst.

type LiveOption

type LiveOption func(*LiveClient)

LiveOption configures a LiveClient.

func WithLiveAuthTimeout

func WithLiveAuthTimeout(d time.Duration) LiveOption

WithLiveAuthTimeout bounds the wait for auth_success. Defaults to 10s.

func WithLiveAutoReconnect

func WithLiveAutoReconnect(enabled bool) LiveOption

WithLiveAutoReconnect enables or disables reconnection. On by default.

func WithLiveConnectToken

func WithLiveConnectToken(token string) LiveOption

WithLiveConnectToken authenticates with a token from MintConnectToken instead of the client's API key. Single-use, and consumed on first connect, so it cannot be replayed by the reconnect loop.

func WithLiveMaxReconnectAttempts

func WithLiveMaxReconnectAttempts(n int) LiveOption

WithLiveMaxReconnectAttempts caps consecutive reconnects. Zero or less means unlimited, which is the default.

func WithLivePingInterval

func WithLivePingInterval(d time.Duration) LiveOption

WithLivePingInterval sets the keepalive period. Defaults to 30s.

func WithLiveQueueSize

func WithLiveQueueSize(n int) LiveOption

WithLiveQueueSize sets the Messages buffer. Defaults to 1000. When the buffer is full the oldest message is dropped, so a slow consumer degrades instead of stalling the feed.

func WithLiveReadTimeout

func WithLiveReadTimeout(d time.Duration) LiveOption

WithLiveReadTimeout sets how long a silent connection is tolerated before it is treated as dead. Defaults to three ping intervals.

func WithLiveTLSConfig

func WithLiveTLSConfig(cfg *tls.Config) LiveOption

WithLiveTLSConfig overrides the TLS settings used for wss connections.

func WithLiveURL

func WithLiveURL(rawURL string) LiveOption

WithLiveURL overrides the derived WebSocket endpoint.

type OddsService

type OddsService struct {
	// contains filtered or unexported fields
}

OddsService covers /odds: "bookmaker", "matchId", "limit" (max 200, default 50), "offset".

func (*OddsService) List

func (s *OddsService) List(ctx context.Context, params Params) (*Page, error)

type Option

type Option func(*Client)

Option configures a Client.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL overrides the API endpoint, e.g. for staging.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient supplies your own *http.Client, for a custom transport or proxy. Its Timeout, if set, applies per attempt and overrides WithTimeout.

func WithHeader

func WithHeader(key, value string) Option

WithHeader adds a header to every request.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets retries for 429, 5xx and network errors. Default 2, zero to disable.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-attempt timeout. Default 30s.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent overrides the User-Agent header.

type Page

type Page struct {
	Success    bool            `json:"success"`
	Data       json.RawMessage `json:"data"`
	Pagination *Pagination     `json:"pagination,omitempty"`
	Source     string          `json:"source,omitempty"`

	// Set on the betting endpoints (/fixtures/{id}/odds, /live-odds, /commentary).
	FixtureID  string `json:"fixtureId,omitempty"`
	MatchAPIID string `json:"matchApiId,omitempty"`
	Count      int    `json:"count,omitempty"`
}

Page is a collection response. Data stays json.RawMessage because rows are provider-shaped and change; decode into your own type:

var teams []Team
if err := page.Into(&teams); err != nil { ... }

func (*Page) Into

func (p *Page) Into(dst any) error

Into decodes the Data array into dst, which should be a pointer to a slice.

func (*Page) Len

func (p *Page) Len() int

Len reports how many rows Data holds.

func (*Page) Rows

func (p *Page) Rows() ([]map[string]any, error)

Rows decodes Data as generic maps. Fine for scripts; use Into elsewhere.

type PageFunc

type PageFunc func(ctx context.Context, params Params) (*Page, error)

PageFunc fetches one page. The paginator supplies limit and offset; merge in whatever else the endpoint needs:

func(ctx context.Context, p Params) (*Page, error) {
    p["leagueId"] = leagueID
    return client.Teams.List(ctx, p)
}

type PaginateOptions

type PaginateOptions struct {
	// PageSize defaults to 100, the limit ceiling on most endpoints. /results and
	// /countries take 500.
	PageSize int
	// MaxItems caps the total rows returned. Zero means no cap.
	MaxItems int
	// StartOffset begins partway into the collection.
	StartOffset int
}

PaginateOptions tunes a Paginator.

type Pagination

type Pagination struct {
	Total   int  `json:"total"`
	Limit   int  `json:"limit"`
	Offset  int  `json:"offset"`
	HasMore bool `json:"hasMore"`
}

Pagination is the envelope's pagination block on list endpoints.

type Paginator

type Paginator struct {
	// contains filtered or unexported fields
}

Paginator walks every page of a list endpoint.

pager := client.Paginate(func(ctx context.Context, p Params) (*Page, error) {
    return client.Leagues.Teams(ctx, leagueID, p)
}, nil)

for pager.Next(ctx) {
    var team Team
    if err := json.Unmarshal(pager.Row(), &team); err != nil { ... }
    fmt.Println(team.Name)
}
if err := pager.Err(); err != nil { ... }

func (*Paginator) Count

func (p *Paginator) Count() int

Count returns how many rows have been yielded.

func (*Paginator) Err

func (p *Paginator) Err() error

Err returns the error that stopped iteration.

func (*Paginator) Into

func (p *Paginator) Into(dst any) error

Into decodes the current row into dst.

func (*Paginator) Next

func (p *Paginator) Next(ctx context.Context) bool

Next advances one row, fetching another page when the buffer empties. Returns false at the end or on the first error; check Err.

func (*Paginator) Page

func (p *Paginator) Page() *Page

Page returns the envelope the current row came from, for Pagination.Total and Source.

func (*Paginator) Row

func (p *Paginator) Row() json.RawMessage

Row returns the current row as raw JSON. Only valid after Next returned true.

type Params

type Params map[string]any

Params are query parameters. Nil and empty values are dropped, so you can build one unconditionally:

goalapi.Params{"leagueId": leagueID, "status": "SCHEDULED", "limit": 100}

Booleans go out as "true"/"false", which is what the validators check for. Slices are comma-joined, as /players/compare expects. Accepted keys: ENDPOINTS.md

type PlayersService

type PlayersService struct {
	// contains filtered or unexported fields
}

PlayersService covers /players.

func (*PlayersService) Compare

func (s *PlayersService) Compare(ctx context.Context, ids ...string) (*Page, error)

Compare takes 2–5 player ids.

func (*PlayersService) Get

func (s *PlayersService) Get(ctx context.Context, playerID string) (*Item, error)

func (*PlayersService) List

func (s *PlayersService) List(ctx context.Context, params Params) (*Page, error)

func (*PlayersService) Search

func (s *PlayersService) Search(ctx context.Context, query string, params Params) (*Page, error)

Search needs a 2-100 character query.

func (*PlayersService) Statistics

func (s *PlayersService) Statistics(ctx context.Context, playerID string, params Params) (*Item, error)

func (*PlayersService) Top

func (s *PlayersService) Top(ctx context.Context, stat string, params Params) (*Page, error)

Top ranks players by a stat. See the Stat* constants.

type PredictionsService

type PredictionsService struct {
	// contains filtered or unexported fields
}

PredictionsService covers /predictions: "matchId", "leagueName", "limit" (max 200, default 50), "offset".

func (*PredictionsService) List

func (s *PredictionsService) List(ctx context.Context, params Params) (*Page, error)

type RateLimit

type RateLimit struct {
	// Limit is the plan's ceiling for the current window.
	Limit int
	// Remaining is how many calls are left in it.
	Remaining int
	// Reset is when the window rolls over, as unix seconds.
	Reset int64
	// Type is "DAILY" or "MONTHLY".
	Type string
}

RateLimit is the quota reported by the last response.

type Raw

type Raw json.RawMessage

Raw is a response body with no {success, data} envelope. The /public/* endpoints return bare objects, so Page or Item would invent a .data field that isn't there.

func (Raw) Into

func (r Raw) Into(dst any) error

Into decodes the body into dst.

func (Raw) Map

func (r Raw) Map() (map[string]any, error)

Map decodes the body as a generic map.

func (Raw) MarshalJSON

func (r Raw) MarshalJSON() ([]byte, error)

MarshalJSON round-trips the original bytes.

func (Raw) String

func (r Raw) String() string

func (*Raw) UnmarshalJSON

func (r *Raw) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps the body verbatim.

type ResultsService

type ResultsService struct {
	// contains filtered or unexported fields
}

ResultsService covers /results. Its list endpoints take limit up to 500.

func (*ResultsService) ByDate

func (s *ResultsService) ByDate(ctx context.Context, date string) (*Page, error)

func (*ResultsService) ByLeague

func (s *ResultsService) ByLeague(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*ResultsService) ByTeam

func (s *ResultsService) ByTeam(ctx context.Context, teamID string, params Params) (*Page, error)

func (*ResultsService) HighScoring

func (s *ResultsService) HighScoring(ctx context.Context, params Params) (*Page, error)

func (*ResultsService) List

func (s *ResultsService) List(ctx context.Context, params Params) (*Page, error)

func (*ResultsService) Stats

func (s *ResultsService) Stats(ctx context.Context, params Params) (*Item, error)

func (*ResultsService) Today

func (s *ResultsService) Today(ctx context.Context) (*Page, error)

func (*ResultsService) Yesterday

func (s *ResultsService) Yesterday(ctx context.Context) (*Page, error)

type StandingsService

type StandingsService struct {
	// contains filtered or unexported fields
}

StandingsService covers /standings. Every method takes an optional "stage".

Home and Away 404 for leagues where the provider has no home/away split, even when the base table has rows.

func (*StandingsService) Away

func (s *StandingsService) Away(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*StandingsService) Form

func (s *StandingsService) Form(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*StandingsService) Get

func (s *StandingsService) Get(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*StandingsService) Home

func (s *StandingsService) Home(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*StandingsService) Team

func (s *StandingsService) Team(ctx context.Context, leagueID, teamID string) (*Item, error)

func (*StandingsService) Zones

func (s *StandingsService) Zones(ctx context.Context, leagueID string, params Params) (*Item, error)

type StatusService

type StatusService struct {
	// contains filtered or unexported fields
}

StatusService covers the unauthenticated status and coverage endpoints, rate limited by IP rather than by key.

These return Raw, not Page or Item: /public/* answers with bare objects.

func (*StatusService) Coverage

func (s *StatusService) Coverage(ctx context.Context) (Raw, error)

Coverage returns {leagues, countries, teams, players, fixtures, ...}.

func (*StatusService) CoverageCountries

func (s *StatusService) CoverageCountries(ctx context.Context) (Raw, error)

CoverageCountries returns {countries[], total}.

func (*StatusService) CoverageLeague

func (s *StatusService) CoverageLeague(ctx context.Context, leagueID string) (Raw, error)

CoverageLeague returns a bare league object.

func (*StatusService) CoverageLeagues

func (s *StatusService) CoverageLeagues(ctx context.Context, params Params) (Raw, error)

CoverageLeagues returns {leagues[], total, page, limit, pages}. Paginates with "page" and "limit", not "offset". Also accepts "q" and "country".

func (*StatusService) Get

func (s *StatusService) Get(ctx context.Context) (Raw, error)

Get returns {status, updatedAt, measurement, components[]}.

type TeamsService

type TeamsService struct {
	// contains filtered or unexported fields
}

TeamsService covers /teams.

func (*TeamsService) Fixtures

func (s *TeamsService) Fixtures(ctx context.Context, teamID string, params Params) (*Page, error)

func (*TeamsService) Get

func (s *TeamsService) Get(ctx context.Context, teamID string, params Params) (*Item, error)

func (*TeamsService) List

func (s *TeamsService) List(ctx context.Context, params Params) (*Page, error)

func (*TeamsService) Players

func (s *TeamsService) Players(ctx context.Context, teamID string, params Params) (*Page, error)

func (*TeamsService) Results

func (s *TeamsService) Results(ctx context.Context, teamID string, params Params) (*Page, error)

func (*TeamsService) Statistics

func (s *TeamsService) Statistics(ctx context.Context, teamID string, params Params) (*Item, error)

func (*TeamsService) Upcoming

func (s *TeamsService) Upcoming(ctx context.Context, teamID string, params Params) (*Page, error)

type VideosService

type VideosService struct {
	// contains filtered or unexported fields
}

VideosService covers /videos.

func (*VideosService) ByDate

func (s *VideosService) ByDate(ctx context.Context, date string, params Params) (*Page, error)

func (*VideosService) ByLeague

func (s *VideosService) ByLeague(ctx context.Context, leagueID string, params Params) (*Page, error)

func (*VideosService) ByMatch

func (s *VideosService) ByMatch(ctx context.Context, matchID string) (*Page, error)

func (*VideosService) List

func (s *VideosService) List(ctx context.Context, params Params) (*Page, error)

func (*VideosService) Recent

func (s *VideosService) Recent(ctx context.Context, params Params) (*Page, error)

Directories

Path Synopsis
examples
basic command
GOAL_API_KEY=...
GOAL_API_KEY=...
export command
GOAL_API_KEY=...
GOAL_API_KEY=...
live command
GOAL_API_KEY=...
GOAL_API_KEY=...

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL