API Authentication - TypeScript examples

Below are complete examples for authenticating with the Data Streams API in TypeScript, using Node.js. 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, Rust SDK, or TypeScript SDK, you don't need to implement the authentication logic manually.

API Authentication Example

Prerequisites

  • Node.js (v20 or later recommended)
  • API credentials from Chainlink Data Streams

Project Setup

  1. Set up your TypeScript environment:

    # Install TypeScript tools
    npm install -g ts-node
    npm install --save-dev typescript @types/node
    
  2. Create a tsconfig.json file in your project folder:

    {
      "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
      }
    }
    

Running the Example

  1. Create a file named auth-example.ts 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 the example:
    ts-node auth-example.ts
    

Example code:

import crypto from "crypto"
import https from "https"
import { IncomingMessage, ClientRequest } from "http"

/**
 * Single report data structure
 */
interface SingleReport {
  feedID: string
  validFromTimestamp: number
  observationsTimestamp: number
  fullReport: string
}

/**
 * SingleReportResponse is the response structure for a single report
 */
interface SingleReportResponse {
  report: SingleReport
}

/**
 * Generates HMAC signature for API authentication
 * @param method - HTTP method (GET, POST, etc.)
 * @param path - Request path including query parameters
 * @param body - Request body (empty string for GET)
 * @param apiKey - API key for authentication
 * @param apiSecret - API secret for signature generation
 * @returns Object containing signature and timestamp
 */
function generateHMAC(
  method: string,
  path: string,
  body: string | Buffer,
  apiKey: string,
  apiSecret: string
): { signature: string; timestamp: number } {
  // Generate timestamp (milliseconds since Unix epoch)
  const timestamp: number = Date.now()

  // Create body hash (empty for GET request)
  const bodyHash: string = crypto
    .createHash("sha256")
    .update(body || "")
    .digest("hex")

  // Create string to sign
  const stringToSign: string = `${method} ${path} ${bodyHash} ${apiKey} ${timestamp}`

  // Generate HMAC-SHA256 signature
  const signature: string = crypto.createHmac("sha256", apiSecret).update(stringToSign).digest("hex")

  return { signature, timestamp }
}

/**
 * Generates authentication headers for API requests
 * @param method - HTTP method
 * @param path - Request path with query parameters
 * @param apiKey - API key
 * @param apiSecret - API secret
 * @returns Headers object for the request
 */
function generateAuthHeaders(method: string, path: string, apiKey: string, apiSecret: string): Record<string, string> {
  const { signature, timestamp } = generateHMAC(method, path, "", apiKey, apiSecret)

  return {
    Authorization: apiKey,
    "X-Authorization-Timestamp": timestamp.toString(),
    "X-Authorization-Signature-SHA256": signature,
  }
}

/**
 * Makes an HTTP request and returns a promise resolving to the response
 * @param options - HTTP request options
 * @returns Response data as string
 */
function makeRequest(options: https.RequestOptions): Promise<string> {
  return new Promise((resolve, reject) => {
    const req: ClientRequest = https.request(options, (res: IncomingMessage) => {
      let data = ""

      res.on("data", (chunk: Buffer) => {
        data += chunk
      })

      res.on("end", () => {
        if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
          resolve(data)
        } else {
          reject(new Error(`API error (status code ${res.statusCode}): ${data}`))
        }
      })
    })

    req.on("error", (error: Error) => {
      reject(new Error(`Request error: ${error.message}`))
    })

    req.end()
  })
}

/**
 * Fetches a single report for a specific feed
 * @param feedID - The feed ID to fetch the report for
 * @returns Promise resolving to the report data
 */
async function fetchSingleReport(feedID: string): Promise<SingleReport> {
  // Get API credentials from environment variables
  const apiKey = process.env.STREAMS_API_KEY
  const apiSecret = process.env.STREAMS_API_SECRET

  // Validate credentials
  if (!apiKey || !apiSecret) {
    throw new Error("API credentials not set. Please set STREAMS_API_KEY and STREAMS_API_SECRET environment variables")
  }

  // API connection details
  const method = "GET"
  const host = "api.testnet-dataengine.chain.link"
  const path = "/api/v1/reports/latest"
  const queryString = `?feedID=${feedID}`
  const fullPath = path + queryString

  // Create request options with authentication headers
  const options: https.RequestOptions = {
    hostname: host,
    path: fullPath,
    method: method,
    headers: generateAuthHeaders(method, fullPath, apiKey, apiSecret),
  }

  try {
    // Make the request
    const responseData = await makeRequest(options)

    // Parse the response
    const response = JSON.parse(responseData) as SingleReportResponse

    return response.report
  } catch (error: any) {
    throw new Error(`Failed to fetch report: ${error.message}`)
  }
}

/**
 * Main execution function to support async/await
 */
async function main(): Promise<void> {
  try {
    // Example feed ID (ETH/USD)
    const feedID = "0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782"

    console.log(`Fetching latest report for feed ID: ${feedID}`)

    // Fetch the report
    const report = await fetchSingleReport(feedID)

    // Display the report
    console.log("Successfully retrieved report:")
    console.log(`  Feed ID: ${report.feedID}`)
    console.log(`  Valid From: ${report.validFromTimestamp}`)
    console.log(`  Observations Timestamp: ${report.observationsTimestamp}`)
    console.log(`  Full Report: ${report.fullReport}`)
  } catch (error: any) {
    console.error("Error:", error.message)
    process.exit(1)
  }
}

// Start the main function
main()

Expected output:

Fetching latest report for feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
Successfully retrieved report:
  Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
  Valid From: 1747929367
  Observations Timestamp: 1747929367
  Full Report: 0x00090d9e8d96765a0c49e03a6ae05c82e8d69000000[...]79dd065e3f83ca7c0ca4aaa

Production Considerations

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

  • HTTP client libraries: Use type-safe libraries like axios with TypeScript support for better error handling and retry capabilities
  • Retry logic: Implement exponential backoff for transient failures with proper type definitions
  • Request timeouts: Add typed timeout handling to prevent hanging requests
  • Error types: Create custom error classes extending Error for better error categorization
  • Logging: Use structured logging libraries like winston or pino with TypeScript definitions
  • Configuration: Use libraries like dotenv with type-safe configuration schemas (e.g., using zod)
  • Input validation: Use runtime type validation libraries for feed IDs and API responses
  • Testing: Add unit tests with jest and @types/jest for type-safe testing

WebSocket Authentication Example

Prerequisites

  • Node.js (v20 or later recommended)
  • API credentials from Chainlink Data Streams

Project Setup

  1. Set up your TypeScript environment:

    # Install TypeScript tools
    npm install -g ts-node
    npm install --save-dev typescript @types/node
    
  2. Install the WebSocket library and its TypeScript definitions:

    npm install ws @types/ws
    
  3. Create a tsconfig.json file in your project folder:

    {
      "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
      }
    }
    

Running the Example

  1. Create a file named ws-auth-example.ts with the example code shown below
  2. Set your API credentials as environment variables (if not already set):
    export STREAMS_API_KEY="your-api-key"
    export STREAMS_API_SECRET="your-api-secret"
    
  3. Run the example:
    ts-node ws-auth-example.ts
    

Example code:

import crypto from "crypto"
import WebSocket from "ws"

// Constants for ping/pong intervals and timeouts
const PING_INTERVAL = 5000 // 5 seconds
const PONG_TIMEOUT = 10000 // 10 seconds
const CONNECTION_TIMEOUT = 30000 // 30 seconds

/**
 * WebSocket with custom properties for TypeScript
 */
interface CustomWebSocket extends WebSocket {
  pongTimeout?: NodeJS.Timeout
}

/**
 * FeedReport represents the data structure received from the WebSocket
 */
interface FeedReport {
  report: {
    feedID: string
    validFromTimestamp?: number
    observationsTimestamp?: number
    fullReport: string
  }
}

/**
 * Generates HMAC signature for API authentication
 * @param method - HTTP method (GET for WebSocket connections)
 * @param path - Request path including query parameters
 * @param apiKey - API key for authentication
 * @param apiSecret - API secret for signature generation
 * @returns Object containing signature and timestamp
 */
function generateHMAC(
  method: string,
  path: string,
  apiKey: string,
  apiSecret: string
): { signature: string; timestamp: number } {
  // Generate timestamp (milliseconds since Unix epoch)
  const timestamp: number = Date.now()

  // Create body hash (empty for WebSocket connection)
  const bodyHash: string = crypto.createHash("sha256").update("").digest("hex")

  // Create string to sign
  const stringToSign: string = `${method} ${path} ${bodyHash} ${apiKey} ${timestamp}`

  // Generate HMAC-SHA256 signature
  const signature: string = crypto.createHmac("sha256", apiSecret).update(stringToSign).digest("hex")

  return { signature, timestamp }
}

/**
 * Sets up the WebSocket connection with proper authentication
 * @param apiKey - API key for authentication
 * @param apiSecret - API secret for signature generation
 * @param feedIDs - Array of feed IDs to subscribe to
 * @returns Promise resolving to WebSocket connection
 */
function setupWebSocketConnection(apiKey: string, apiSecret: string, feedIDs: string[]): Promise<CustomWebSocket> {
  return new Promise((resolve, reject) => {
    // Validate feed IDs
    if (!feedIDs || feedIDs.length === 0) {
      reject(new Error("No feed ID(s) provided"))
      return
    }

    // WebSocket connection details
    const host = "ws.testnet-dataengine.chain.link"
    const path = "/api/v1/ws"
    const queryString = `?feedIDs=${feedIDs.join(",")}`
    const fullPath = path + queryString

    // Generate authentication signature and timestamp
    const { signature, timestamp } = generateHMAC("GET", fullPath, apiKey, apiSecret)

    // Create WebSocket URL
    const wsURL = `wss://${host}${fullPath}`
    console.log("Connecting to:", wsURL)

    // Set up the WebSocket connection with authentication headers
    const ws = new WebSocket(wsURL, {
      headers: {
        Authorization: apiKey,
        "X-Authorization-Timestamp": timestamp.toString(),
        "X-Authorization-Signature-SHA256": signature,
      },
      handshakeTimeout: CONNECTION_TIMEOUT,
    }) as CustomWebSocket

    // Handle connection errors
    ws.on("error", (err) => {
      reject(new Error(`WebSocket connection error: ${err.message}`))
    })

    // Resolve the promise when the connection is established
    ws.on("open", () => {
      console.log("WebSocket connection established")
      resolve(ws)
    })
  })
}

/**
 * Sets up ping/pong mechanism to keep the connection alive
 * @param ws - WebSocket connection
 */
function setupPingPong(ws: CustomWebSocket): void {
  // Set up ping interval
  const pingInterval = setInterval(() => {
    if (ws.readyState === ws.OPEN) {
      console.log("Sending ping to keep connection alive...")
      ws.ping(Buffer.from(crypto.randomBytes(8)))

      // Set up pong timeout - if we don't receive a pong within timeout, close the connection
      ws.pongTimeout = setTimeout(() => {
        console.log("No pong received, closing connection...")
        ws.terminate()
      }, PONG_TIMEOUT)
    }
  }, PING_INTERVAL)

  // Clear pong timeout when pong is received
  ws.on("pong", () => {
    console.log("Received pong from server")
    clearTimeout(ws.pongTimeout)
  })

  // Clear intervals on close
  ws.on("close", () => {
    clearInterval(pingInterval)
    clearTimeout(ws.pongTimeout)
  })
}

/**
 * Handle WebSocket messages
 * @param ws - WebSocket connection
 */
function handleMessages(ws: CustomWebSocket): void {
  ws.on("message", (data: Buffer | string) => {
    try {
      // Parse the message
      const message = JSON.parse(data.toString()) as FeedReport

      // Check if it has the expected report format
      if (message.report && message.report.feedID) {
        const report = message.report
        console.log("\nReceived new report:")
        console.log(`  Feed ID: ${report.feedID}`)

        // Display timestamps if available
        if (report.validFromTimestamp) {
          console.log(`  Valid From: ${report.validFromTimestamp}`)
        }

        if (report.observationsTimestamp) {
          console.log(`  Observations Timestamp: ${report.observationsTimestamp}`)
        }

        // Display the full report with truncation for readability
        if (report.fullReport) {
          const reportPreview =
            report.fullReport.length > 40 ? `${report.fullReport.substring(0, 40)}...` : report.fullReport
          console.log(`  Full Report: ${reportPreview}`)
        }
      } else {
        console.log("Received message with unexpected format:", message)
      }
    } catch (error: any) {
      console.error("Error parsing message:", error)
      console.log("Raw message:", data.toString())
    }
  })
}

/**
 * Main function to set up and manage the WebSocket connection
 */
async function main(): Promise<void> {
  try {
    // Get API credentials from environment variables
    const apiKey = process.env.STREAMS_API_KEY
    const apiSecret = process.env.STREAMS_API_SECRET

    // Validate credentials
    if (!apiKey || !apiSecret) {
      throw new Error(
        "API credentials not set. Please set STREAMS_API_KEY and STREAMS_API_SECRET environment variables"
      )
    }

    // Example feed ID (ETH/USD)
    const feedIDs = ["0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782"]

    // Set up WebSocket connection
    const ws = await setupWebSocketConnection(apiKey, apiSecret, feedIDs)

    // Set up ping/pong to keep connection alive
    setupPingPong(ws)

    // Handle incoming messages
    handleMessages(ws)

    // Set up graceful shutdown on SIGINT (Ctrl+C)
    process.on("SIGINT", () => {
      console.log("\nInterrupt signal received, closing connection...")

      // Close the WebSocket connection gracefully
      if (ws.readyState === ws.OPEN) {
        ws.close(1000, "Client shutting down")
      }

      // Allow some time for the close to be sent, then exit
      setTimeout(() => {
        console.log("Exiting...")
        process.exit(0)
      }, 1000)
    })
  } catch (error: any) {
    console.error("Error:", error.message)
    process.exit(1)
  }
}

// Start the main function
main()

Expected output:

Connecting to: wss://ws.testnet-dataengine.chain.link/api/v1/ws?feedIDs=0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
WebSocket connection established

Received new report:
  Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
  Valid From: 1747930054
  Observations Timestamp: 1747930054
  Full Report: 0x00090d9e8d96765a0c49e03a6ae05c82e8f8de...

[...]

Received new report:
  Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
  Valid From: 1747930059
  Observations Timestamp: 1747930059
  Full Report: 0x00090d9e8d96765a0c49e03a6ae05c82e8f8de...
Sending ping to keep connection alive...
Received pong from server

Received new report:
  Feed ID: 0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782
  Valid From: 1747930060
  Observations Timestamp: 1747930060
  Full Report: 0x00090d9e8d96765a0c49e03a6ae05c82e8f8de...
^C
Interrupt signal received, closing connection...
Exiting...

Production Considerations

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

  • Automatic reconnection: Implement typed exponential backoff reconnection logic for network disruptions
  • Message queuing: Create typed message buffers during reconnection attempts
  • WebSocket libraries: Consider type-safe libraries like socket.io-client with full TypeScript support
  • Structured logging: Use winston or pino with TypeScript definitions for type-safe logging
  • Metrics collection: Implement typed metrics for connection status, message rates, and latency
  • Configuration management: Use dotenv with type-safe schemas (e.g., zod) for environment configuration
  • Error categorization: Create custom error classes with specific types for different failure scenarios
  • Testing: Add unit tests with jest and WebSocket mocks for type-safe testing

Get the latest Chainlink content straight to your inbox.