# 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:
- Read an Orbistats API key from an environment variable.
- Send a Bearer-authenticated request from Go.
- Request football fixtures.
- Decode the JSON response into Go structs.
- Handle non-2xx HTTP responses.
- Leave the client structure ready for additional endpoints.
The base API URL is:
https://api.orbistats.com/v1/
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"
On Windows PowerShell:
$env:ORBISTATS_API_KEY="your_api_key_here"
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
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"`
}
The important part is the JSON tag.
For example:
FixtureID string `json:"fixture_id"`
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,
)
}
}
Run it with:
go run .
The important authentication line is:
req.Header.Set("Authorization", "Bearer "+apiKey)
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),
)
}
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()
}
You could then build a request URL such as:
https://api.orbistats.com/v1/football/fixtures?date=2026-09-14
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
}
}
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.
}
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)
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/
└── ...
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
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
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.Clientinstead of creating one per request. - Request timeouts.
- Context cancellation with
context.Context. - Structured application logging.
- Explicit handling for
401,403,404, and429. - 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,
}
And in a larger application, prefer a context-aware request:
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
url,
nil,
)
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)