API Authentication - Go examples

Below are complete examples for authenticating with the Data Streams API in Go. Each example shows how to properly generate the required headers and make a request.

To learn more about the Data Streams API authentication, see the Data Streams Authentication page.

Note: The Data Streams SDKs handle authentication automatically. If you're using the Go SDK or Rust SDK, you don't need to implement the authentication logic manually.

API Authentication Example

Requirements

  • Go (v1.18 or later recommended)
  • API credentials from Chainlink Data Streams

Running the Example

  1. Create a file named auth-example.go with the example code shown below
  2. Set your API credentials as environment variables:
    export STREAMS_API_KEY="your-api-key"
    export STREAMS_API_SECRET="your-api-secret"
    
  3. Run with go run auth-example.go

Example code:

package main

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"
)

// SingleReport represents a data feed report structure
type SingleReport struct {
    FeedID                string `json:"feedID"`
    ValidFromTimestamp    uint32 `json:"validFromTimestamp"`
    ObservationsTimestamp uint32 `json:"observationsTimestamp"`
    FullReport            string `json:"fullReport"`
}

// SingleReportResponse is the response structure for a single report
type SingleReportResponse struct {
    Report SingleReport `json:"report"`
}

// GenerateHMAC creates the signature for authentication
func GenerateHMAC(method string, path string, body []byte, apiKey string, apiSecret string) (string, int64) {
    // Generate timestamp (milliseconds since Unix epoch)
    timestamp := time.Now().UTC().UnixMilli()

    // Generate body hash
    serverBodyHash := sha256.New()
    serverBodyHash.Write(body)
    bodyHashString := hex.EncodeToString(serverBodyHash.Sum(nil))

    // Create string to sign
    stringToSign := fmt.Sprintf("%s %s %s %s %d",
        method,
        path,
        bodyHashString,
        apiKey,
        timestamp)

    // Generate HMAC-SHA256 signature
    signedMessage := hmac.New(sha256.New, []byte(apiSecret))
    signedMessage.Write([]byte(stringToSign))
    signature := hex.EncodeToString(signedMessage.Sum(nil))

    return signature, timestamp
}

// GenerateAuthHeaders creates HTTP headers with authentication information
func GenerateAuthHeaders(method string, pathWithParams string, apiKey string, apiSecret string) http.Header {
    header := http.Header{}
    signature, timestamp := GenerateHMAC(method, pathWithParams, []byte(""), apiKey, apiSecret)

    header.Add("Authorization", apiKey)
    header.Add("X-Authorization-Timestamp", strconv.FormatInt(timestamp, 10))
    header.Add("X-Authorization-Signature-SHA256", signature)

    return header
}

// FetchSingleReport retrieves a single report for a specific feed
func FetchSingleReport(ctx context.Context, feedID string) (*SingleReport, error) {
    // Get API credentials from environment variables
    apiKey := os.Getenv("STREAMS_API_KEY")
    apiSecret := os.Getenv("STREAMS_API_SECRET")

    // Validate credentials
    if apiKey == "" || apiSecret == "" {
        return nil, fmt.Errorf("API credentials not set. Please set STREAMS_API_KEY and STREAMS_API_SECRET environment variables")
    }

    // API connection details
    host := "api.testnet-dataengine.chain.link"
    path := "/api/v1/reports/latest"

    // Build query parameters
    params := url.Values{
        "feedID": {feedID},
    }

    // Create the request URL
    reqURL := &url.URL{
        Scheme:   "https",
        Host:     host,
        Path:     path,
        RawQuery: params.Encode(),
    }

    // Create the HTTP request
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
    if err != nil {
        return nil, fmt.Errorf("error creating request: %w", err)
    }

    // Add authentication headers
    req.Header = GenerateAuthHeaders(req.Method, req.URL.RequestURI(), apiKey, apiSecret)

    // Execute the request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("error sending request: %w", err)
    }
    defer resp.Body.Close()

    // Read the response body
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("error reading response: %w", err)
    }

    // Check for non-success status code
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API error (status code %d): %s", resp.StatusCode, string(body))
    }

    // Parse the response
    var reportResp SingleReportResponse
    if err := json.Unmarshal(body, &reportResp); err != nil {
        return nil, fmt.Errorf("error parsing response: %w", err)
    }

    return &reportResp.Report, nil
}

func main() {
    // Create a context with cancellation
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Example feed ID (ETH/USD)
    feedID := "0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782"

    fmt.Printf("Fetching latest report for feed ID: %s\n", feedID)

    // Fetch the report
    report, err := FetchSingleReport(ctx, feedID)
    if err != nil {
        log.Fatalf("Error: %v", err)
    }

    // Display the report
    fmt.Println("Successfully retrieved report:")
    fmt.Printf("  Feed ID: %s\n", report.FeedID)
    fmt.Printf("  Valid From: %d\n", report.ValidFromTimestamp)
    fmt.Printf("  Observations Timestamp: %d\n", report.ObservationsTimestamp)
    fmt.Printf("  Full Report: %s\n", report.FullReport)
}

Expected output:

Fetching latest report for feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Successfully retrieved report:
  Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
  Valid From: 1747921357
  Observations Timestamp: 1747921357
  Full Report: 0x00090d9e8d96765a0c49e03a6ae05c[...]bffd8feddabf120e14bf

Production Considerations

While this example demonstrates the authentication mechanism, production applications should consider:

  • HTTP client reuse: Create a single http.Client with timeout settings and reuse it
  • Retry logic: Implement exponential backoff for transient failures
  • Structured logging: Use a logging library like zap or logrus instead of fmt.Printf
  • Configuration: Use a configuration library like viper for managing settings
  • Metrics: Add instrumentation for monitoring API call performance
  • Error types: Define custom error types for better error handling
  • Testing: Add unit tests for HMAC generation and integration tests

For production use, consider using the Go SDK which handles authentication automatically and provides built-in fault tolerance.

WebSocket Authentication Example

Requirements

  • Go (v1.18 or later recommended)
  • API credentials from Chainlink Data Streams

Running the Example

  1. Create a new directory for the example:

    mkdir ws-go-example && cd ws-go-example
    
  2. Initialize a Go module:

    go mod init ws-example
    
  3. Create a file named ws-auth-example.go with the example code shown below

  4. Set your API credentials as environment variables:

    export STREAMS_API_KEY="your-api-key"
    export STREAMS_API_SECRET="your-api-secret"
    
  5. Install the required dependencies:

    go mod tidy
    
  6. Run the example

    go run ws-auth-example.go
    
  7. Press Ctrl+C to stop the WebSocket stream when you're done

Example code:

package main

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "strconv"
    "strings"
    "time"

    "github.com/gorilla/websocket"
)

// Constants for ping interval, pong timeout, and write timeout
const (
    pingInterval = 5 * time.Second
    pongTimeout = 10 * time.Second
    writeTimeout = 5 * time.Second
)

// FeedReport represents the data structure received from the WebSocket
type FeedReport struct {
    Report struct {
        FeedID     string `json:"feedID"`
        FullReport string `json:"fullReport"`
    } `json:"report"`
}

// GenerateHMAC creates the signature for authentication
func GenerateHMAC(method, path string, apiKey string, apiSecret string) (string, int64) {
    // Generate timestamp (milliseconds since Unix epoch)
    timestamp := time.Now().UTC().UnixMilli()

    // Generate body hash (empty for this connection)
    serverBodyHash := sha256.New()
    bodyHashString := hex.EncodeToString(serverBodyHash.Sum(nil))

    // Create string to sign
    stringToSign := fmt.Sprintf("%s %s %s %s %d",
        method,
        path,
        bodyHashString,
        apiKey,
        timestamp)

    // Generate HMAC-SHA256 signature
    signedMessage := hmac.New(sha256.New, []byte(apiSecret))
    signedMessage.Write([]byte(stringToSign))
    signature := hex.EncodeToString(signedMessage.Sum(nil))

    return signature, timestamp
}

// pingLoop sends periodic pings to keep the connection alive
func pingLoop(ctx context.Context, conn *websocket.Conn) {
    ticker := time.NewTicker(pingInterval)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            log.Println("Sending ping to keep connection alive...")
            if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeTimeout)); err != nil {
                log.Printf("Error sending ping: %v", err)
                return
            }
        }
    }
}

func main() {
    // Get API credentials from environment variables
    apiKey := os.Getenv("STREAMS_API_KEY")
    apiSecret := os.Getenv("STREAMS_API_SECRET")

    // Validate credentials
    if apiKey == "" || apiSecret == "" {
        log.Fatal("API credentials not set. Please set STREAMS_API_KEY and STREAMS_API_SECRET environment variables")
    }

    // WebSocket connection details
    host := "ws.testnet-dataengine.chain.link"
    path := "/api/v1/ws"
    feedIDs := []string{"0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782"} // ETH/USD

    // Validate feed IDs
    if len(feedIDs) == 0 {
        log.Fatal("No feed ID(s) provided")
    }

    queryParams := fmt.Sprintf("feedIDs=%s", strings.Join(feedIDs, ","))
    fullPath := fmt.Sprintf("%s?%s", path, queryParams)

    // Generate authentication signature and timestamp
    signature, timestamp := GenerateHMAC("GET", fullPath, apiKey, apiSecret)

    // Create HTTP header for WebSocket connection
    header := http.Header{}
    header.Add("Authorization", apiKey)
    header.Add("X-Authorization-Timestamp", strconv.FormatInt(timestamp, 10))
    header.Add("X-Authorization-Signature-SHA256", signature)

    // Create WebSocket URL
    wsURL := fmt.Sprintf("wss://%s%s?%s", host, path, queryParams)
    fmt.Println("Connecting to:", wsURL)

    // Create context for handling cancellation
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Set up channel to handle interrupt signal
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    // Connect to WebSocket server
    conn, resp, err := websocket.DefaultDialer.DialContext(ctx, wsURL, header)
    if err != nil {
        if resp != nil {
            log.Fatalf("WebSocket connection error (HTTP %d): %v", resp.StatusCode, err)
        } else {
            log.Fatalf("WebSocket connection error: %v", err)
        }
    }
    defer conn.Close()

    // Add pong handler to reset read deadline when pong is received
    conn.SetPongHandler(func(string) error {
        log.Println("Received pong from server")
        return conn.SetReadDeadline(time.Now().Add(pongTimeout))
    })

    // Start the ping loop in a separate goroutine
    go pingLoop(ctx, conn)

    // Set initial read deadline
    err = conn.SetReadDeadline(time.Now().Add(pongTimeout))
    if err != nil {
        log.Fatalf("Error setting read deadline: %v", err)
    }

    fmt.Println("WebSocket connection established")

    // Create channel for done signal
    done := make(chan struct{})

    // Handle incoming messages in a separate goroutine
    go func() {
        defer close(done)

        for {
            _, message, err := conn.ReadMessage()
            if err != nil {
                log.Printf("WebSocket read error: %v", err)
                return
            }

            // Parse the message
            var report FeedReport
            if err := json.Unmarshal(message, &report); err != nil {
                log.Printf("Error parsing message: %v", err)
                fmt.Println("Raw message:", string(message))
                continue
            }

            fmt.Printf("Received report for Feed ID: %s\n", report.Report.FeedID)
        }
    }()

    // Wait for interrupt signal or error
    for {
        select {
        case <-done:
            return
        case <-interrupt:
            fmt.Println("\nInterrupt signal received, closing connection...")

            // Close the WebSocket connection gracefully
            err := conn.WriteControl(
                websocket.CloseMessage,
                websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
                time.Now().Add(time.Second),
            )
            if err != nil {
                log.Printf("Error sending close message: %v", err)
            }

            // Wait for message handling to complete or timeout
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}

Expected output:

Connecting to: wss://ws.testnet-dataengine.chain.link/api/v1/ws?feedIDs=0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
WebSocket connection established
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
2025/05/22 08:34:20 Sending ping to keep connection alive...
2025/05/22 08:34:20 Received pong from server
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Received report for Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782

Production Considerations

While this example already includes many production-ready features (keepalive, timeouts, graceful shutdown), production applications should additionally consider:

  • Automatic reconnection: Implement exponential backoff reconnection logic for network disruptions
  • Message buffering: Queue outgoing messages during reconnection attempts
  • Structured logging: Use zap or logrus with log levels instead of log.Printf
  • Metrics collection: Track connection status, message rates, and latency
  • Configuration management: Make timeouts and intervals configurable via environment or config files
  • Error categorization: Define custom error types to distinguish between retriable and fatal errors
  • Health checks: Expose WebSocket connection status for monitoring systems
  • Testing: Add unit tests for HMAC generation and mock WebSocket server for integration tests

For production use, consider using the Go SDK which handles authentication automatically and provides built-in fault tolerance for streaming connections.

Get the latest Chainlink content straight to your inbox.