DEV Community

orbistats
orbistats

Posted on

Consuming a Sports Data API in Go with net/http

 # Consuming a Sports Data API in Go with net/http

When you build a sports application in Go, the HTTP request itself is usually the easy part.

The harder part is turning an external sports feed into something your application can use reliably: authentication, JSON decoding, pagination, rate limits, error handling, and eventually real-time updates all become part of the integration.

In this tutorial, we'll build a small Go client for the Orbistats sports data API using Go's standard net/http package. The goal is to keep the example small enough to understand, while still showing patterns you can reuse in a larger service.

What we're building

The example will:

  1. Read an Orbistats API key from an environment variable.
  2. Send a Bearer-authenticated request from Go.
  3. Request football fixtures.
  4. Decode the JSON response into Go structs.
  5. Handle non-2xx HTTP responses.
  6. Leave the client structure ready for additional endpoints.

The base API URL is:

https://api.orbistats.com/v1/
Enter fullscreen mode Exit fullscreen mode

Orbistats documents Bearer-token authentication for API requests.

1. Get an API key

For authenticated API requests, create a free account and generate an API key:

https://orbistats.com/signup.html

You can also experiment with the public sandbox before creating an account:

https://orbistats.com/developers/sandbox.html

For local development, keep the API key in an environment variable instead of committing it to source control.

On Linux/macOS:

export ORBISTATS_API_KEY="your_api_key_here"
Enter fullscreen mode Exit fullscreen mode

On Windows PowerShell:

$env:ORBISTATS_API_KEY="your_api_key_here"
Enter fullscreen mode Exit fullscreen mode

Avoid putting the key directly into a Go source file, especially if the repository will be public.

2. Create a Go project

Create a new directory and initialize a module:

mkdir orbistats-go-example
cd orbistats-go-example

go mod init example.com/orbistats-go-example
Enter fullscreen mode Exit fullscreen mode

This example uses only Go's standard library, so there are no third-party HTTP dependencies to install.

3. Define the response types

A useful property of Go is that JSON can be decoded directly into structs.

A simplified fixture response can be represented like this:

package main

type Fixture struct {
    FixtureID  string `json:"fixture_id"`
    HomeTeam   string `json:"home_team"`
    AwayTeam   string `json:"away_team"`
    Kickoff    string `json:"kickoff"`
    Competition string `json:"competition"`
}

type Meta struct {
    Sport string `json:"sport"`
    Count int    `json:"count"`
}

type Pagination struct {
    Page         int `json:"page"`
    PerPage      int `json:"per_page"`
    TotalPages   int `json:"total_pages"`
    TotalResults int `json:"total_results"`
}

type FixturesResponse struct {
    Data       []Fixture  `json:"data"`
    Meta       Meta       `json:"meta"`
    Pagination Pagination `json:"pagination"`
}
Enter fullscreen mode Exit fullscreen mode

The important part is the JSON tag.

For example:

FixtureID string `json:"fixture_id"`
Enter fullscreen mode Exit fullscreen mode

tells Go's JSON decoder that the API field fixture_id should populate the FixtureID field.

If the API returns additional fields that your application does not need yet, you don't have to model every field immediately.

4. Make the authenticated request

Now we can create a small function that calls the fixtures endpoint.

package main

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

const baseURL = "https://api.orbistats.com/v1"

func getFixtures(apiKey string) (*FixturesResponse, error) {
    req, err := http.NewRequest(
        http.MethodGet,
        baseURL+"/football/fixtures",
        nil,
    )
    if err != nil {
        return nil, err
    }

    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Accept", "application/json")

    client := &http.Client{}

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        body, _ := io.ReadAll(resp.Body)

        return nil, fmt.Errorf(
            "Orbistats API returned %s: %s",
            resp.Status,
            string(body),
        )
    }

    var result FixturesResponse

    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }

    return &result, nil
}

func main() {
    apiKey := os.Getenv("ORBISTATS_API_KEY")

    if apiKey == "" {
        fmt.Println("ORBISTATS_API_KEY is not set")
        os.Exit(1)
    }

    result, err := getFixtures(apiKey)
    if err != nil {
        fmt.Println("Request failed:", err)
        os.Exit(1)
    }

    fmt.Printf("Sport: %s\n", result.Meta.Sport)
    fmt.Printf("Fixtures returned: %d\n", result.Meta.Count)

    for _, fixture := range result.Data {
        fmt.Printf(
            "%s vs %s — %s\n",
            fixture.HomeTeam,
            fixture.AwayTeam,
            fixture.Kickoff,
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Run it with:

go run .
Enter fullscreen mode Exit fullscreen mode

The important authentication line is:

req.Header.Set("Authorization", "Bearer "+apiKey)
Enter fullscreen mode Exit fullscreen mode

That is the same Bearer-token pattern you can reuse for other Orbistats API requests.

5. Why handle HTTP errors before decoding JSON?

It can be tempting to immediately decode every response into your success struct.

In production code, it's better to inspect the status first:

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    body, _ := io.ReadAll(resp.Body)

    return nil, fmt.Errorf(
        "Orbistats API returned %s: %s",
        resp.Status,
        string(body),
    )
}
Enter fullscreen mode Exit fullscreen mode

That gives you a useful distinction between:

  • a successful response with data,
  • an authentication failure,
  • a bad request,
  • a missing resource,
  • a rate-limit response,
  • and a server-side error.

For example, if the API key is invalid, the API can return an HTTP 401 response. A 429 response indicates that the request rate has exceeded the applicable limit.

Your application can then decide whether to return an error to the caller, retry, or back off.

6. Add query parameters

Most useful sports applications eventually need filtering.

For example, instead of constructing a URL manually, Go's net/url package can safely build query parameters:

package main

import "net/url"

func addDateFilter(base string, date string) string {
    u, err := url.Parse(base)
    if err != nil {
        return base
    }

    q := u.Query()
    q.Set("date", date)
    u.RawQuery = q.Encode()

    return u.String()
}
Enter fullscreen mode Exit fullscreen mode

You could then build a request URL such as:

https://api.orbistats.com/v1/football/fixtures?date=2026-09-14
Enter fullscreen mode Exit fullscreen mode

This is preferable to manually concatenating strings when an application has multiple optional filters.

The API documentation also describes common parameters such as date, season, team_id, competition_id, page, and per_page.

7. Think about pagination early

A sports API can return more fixtures than you want to process in a single request.

A typical list response includes pagination information, for example:

{
  "data": [],
  "meta": {
    "sport": "football",
    "count": 25
  },
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total_pages": 4,
    "total_results": 87
  }
}
Enter fullscreen mode Exit fullscreen mode

A simple pagination loop in Go could look like:

for page := 1; page <= totalPages; page++ {
    // Request the current page.
    // Process result.Data.
    // Continue until all pages are handled.
}
Enter fullscreen mode Exit fullscreen mode

For a production service, I would put pagination inside the API client rather than making every caller understand the API's pagination rules.

That gives the rest of your application a simpler interface:

fixtures, err := client.ListFixtures(ctx, filters)
Enter fullscreen mode Exit fullscreen mode

instead of making business logic responsible for HTTP details.

8. Keep the API client separate from application logic

Once the integration grows, a useful project structure might look like this:

orbistats-go-example/
├── go.mod
├── main.go
├── orbistats/
│   ├── client.go
│   ├── fixtures.go
│   └── errors.go
└── internal/
    └── ...
Enter fullscreen mode Exit fullscreen mode

The orbistats package can own:

  • authentication headers,
  • request construction,
  • JSON decoding,
  • pagination,
  • API errors,
  • retries/backoff where appropriate.

Your application code can then work with Go types instead of raw HTTP responses.

This separation becomes especially useful if the same sports data is consumed by multiple parts of a service.

9. Polling vs WebSocket for live sports data

For scheduled fixtures or data that changes relatively slowly, normal HTTP requests are straightforward.

Live scores and odds are different.

A basic polling model looks like:

Your service
     |
     | GET /football/...
     v
Orbistats
     |
     | JSON response
     v
Your service
Enter fullscreen mode Exit fullscreen mode

You repeat that request on a timer.

Polling is simple, but the interval creates a trade-off:

  • Poll too frequently and you make unnecessary requests.
  • Poll too slowly and your application can display stale data.

For live applications, a push-based architecture can be a better fit.

Orbistats documents two real-time approaches: webhooks for event-driven updates and a WebSocket API for a persistent streaming connection.

A WebSocket model looks more like:

Your service
     |
     | persistent connection
     v
Orbistats stream
     |
     +---- update
     +---- update
     +---- update
Enter fullscreen mode Exit fullscreen mode

If you are building a live score board, notification system, or another application where updates need to arrive continuously, compare the polling, webhook, and WebSocket approaches in the Orbistats documentation:

https://orbistats.com/developers/documentation.html

For many applications, a practical architecture is to use ordinary REST requests for initial state and historical/scheduled data, then use a real-time mechanism for subsequent updates.

10. A small production checklist

Before turning this example into a production integration, I would add:

  • A shared http.Client instead of creating one per request.
  • Request timeouts.
  • Context cancellation with context.Context.
  • Structured application logging.
  • Explicit handling for 401, 403, 404, and 429.
  • Backoff for retryable failures.
  • Pagination helpers.
  • Metrics for request latency and error rates.
  • Tests using an httptest.Server.
  • Environment-based configuration for API credentials.
  • Protection against accidentally logging the Bearer token.

For example, using a timeout:

client := &http.Client{
    Timeout: 10 * time.Second,
}
Enter fullscreen mode Exit fullscreen mode

And in a larger application, prefer a context-aware request:

req, err := http.NewRequestWithContext(
    ctx,
    http.MethodGet,
    url,
    nil,
)
Enter fullscreen mode Exit fullscreen mode

That allows the request to stop when the surrounding operation is cancelled.

11. Where to go from here

Once the basic Go client works, the same pattern can be extended to fixtures, results, standings, statistics, odds, teams, players, competitions, and other resources supported by the API.

The full endpoint list, parameters, response fields, pagination details, and real-time references are available in the Orbistats API reference:

https://orbistats.com/developers/api-reference.html

The main idea is to keep the HTTP layer small and predictable. Go's standard library already provides most of what you need to make the first integration without adding an SDK or HTTP framework.

Top comments (0)