SDK Reference: HTTP Client

This page provides a reference for making offchain HTTP requests using the HTTPClient. This is a "node-level" action capability, meaning it executes on each individual node in the DON.

Because it operates at the node level, all HTTP requests are wrapped in a consensus mechanism to provide a single, reliable result to your workflow. The TypeScript SDK provides two ways to use the HTTPClient:

  • High-level (recommended): Automatically wraps your request in the runtime.runInNodeMode() pattern with consensus aggregation.
  • Low-level: Requires manual wrapping in a runtime.runInNodeMode() block for complex scenarios.

For complete step-by-step examples, see the GET requests and POST requests guides.

The high-level sendRequest() method is the recommended approach for making HTTP requests. It simplifies the process by automatically handling the runtime.runInNodeMode() pattern and consensus aggregation for you.

Signature:

sendRequest<TArgs extends unknown[], TOutput>(
  runtime: Runtime<unknown>,
  fn: (sendRequester: SendRequester, ...args: TArgs) => TOutput,
  consensusAggregation: ConsensusAggregation<TOutput, true>,
  unwrapOptions?: TOutput extends PrimitiveTypes ? never : UnwrapOptions<TOutput>
): (...args: TArgs) => { result: () => TOutput }

Parameters:

  • runtime: The top-level Runtime from your workflow callback.
  • fn: A function containing your core fetching and parsing logic. It receives a SendRequester instance and any additional arguments you provide.
  • consensusAggregation: The consensus aggregation method (e.g., consensusMedianAggregation(), ConsensusAggregationByFields()).
  • unwrapOptions: Optional. Unwrapping configuration for complex types. Not needed for primitive types or flat objects.

Returns:

A curried function that accepts your custom arguments and returns an object with a .result() method. Calling .result() blocks until the consensus is reached and returns the aggregated result.

Example:

import { cre, consensusMedianAggregation, type Runtime, type HTTPSendRequester } from "@chainlink/cre-sdk"

interface Config {
  apiUrl: string
}

// Your fetching and parsing logic
const fetchPrice = (sendRequester: HTTPSendRequester, url: string): number => {
  const response = sendRequester.sendRequest({ url }).result()

  if (response.statusCode !== 200) {
    throw new Error(`HTTP request failed with status: ${response.statusCode}`)
  }

  const responseText = new TextDecoder().decode(response.body)
  const data = JSON.parse(responseText)

  return data.price
}

// In your workflow
const workflow = (runtime: Runtime<Config>) => {
  const httpClient = new cre.capabilities.HTTPClient()

  // Call the high-level sendRequest with your custom function
  const price = httpClient
    .sendRequest(runtime, fetchPrice, consensusMedianAggregation<number>())(runtime.config.apiUrl)
    .result()

  runtime.log(`Aggregated price: ${price}`)

  return price
}

Using SendRequester

The SendRequester helper class is provided to your function by the high-level sendRequest() method. It provides a simplified interface for making HTTP requests within the node-level execution context.

Method:

sendRequest(input: Request | RequestJson): { result: () => Response }

Parameters:

  • input: A Request or RequestJson object defining the API call.

Returns:

An object with a .result() method that blocks until the HTTP request completes and returns the Response.

Using sendReport()

The SendRequester class also provides a sendReport() method for submitting reports via HTTP. This is useful when you need to send a cryptographically signed report to an external API endpoint.

Method:

sendReport(
  report: Report,
  fn: (reportResponse: ReportResponse) => Request | RequestJson
): { result: () => Response }

Parameters:

  • report: A Report object generated by runtime.report().
  • fn: A function that converts the inner ReportResponse to a Request or RequestJson object.

Returns:

An object with a .result() method that blocks until the HTTP request completes and returns the Response.

Example:

import { type HTTPSendRequester, type Report } from "@chainlink/cre-sdk"

const submitReport = (sendRequester: HTTPSendRequester, report: Report): string => {
  const response = sendRequester
    .sendReport(report, (reportResponse) => ({
      url: "https://api.example.com/submit-report",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: Buffer.from(JSON.stringify({ report: reportResponse })).toString("base64"),
    }))
    .result()

  if (response.statusCode !== 200) {
    throw new Error(`Failed to submit report: ${response.statusCode}`)
  }

  return "Report submitted successfully"
}

Low-level sendRequest()

The low-level sendRequest() method requires manual wrapping in a runtime.runInNodeMode() block. It provides more flexibility for complex scenarios but requires more boilerplate code.

Signature:

sendRequest(
  runtime: NodeRuntime<unknown>,
  input: Request | RequestJson
): { result: () => Response }

Parameters:

  • runtime: A NodeRuntime instance. This is provided by the runtime.runInNodeMode() function.
  • input: A Request or RequestJson object defining the API call.

Returns:

An object with a .result() method that blocks until the HTTP request completes and returns the Response.

Example:

import { cre, consensusMedianAggregation, type Runtime, type NodeRuntime } from "@chainlink/cre-sdk"

// Low-level usage with manual node mode
const fetchPrice = (nodeRuntime: NodeRuntime<Config>): number => {
  const httpClient = new cre.capabilities.HTTPClient()

  const response = httpClient
    .sendRequest(nodeRuntime, {
      url: nodeRuntime.config.apiUrl,
      method: "GET",
    })
    .result()

  if (response.statusCode !== 200) {
    throw new Error(`HTTP request failed with status: ${response.statusCode}`)
  }

  const responseText = new TextDecoder().decode(response.body)
  const data = JSON.parse(responseText)

  return data.price
}

// In your workflow
const workflow = (runtime: Runtime<Config>) => {
  const price = runtime.runInNodeMode(fetchPrice, consensusMedianAggregation<number>())().result()

  runtime.log(`Aggregated price: ${price}`)

  return price
}

Low-level sendReport()

The ClientCapability class also provides a low-level sendReport() method for submitting reports via HTTP when using manual node mode wrapping.

Signature:

sendReport(
  runtime: NodeRuntime<unknown>,
  report: Report,
  fn: (reportResponse: ReportResponse) => Request | RequestJson
): { result: () => Response }

Parameters:

  • runtime: A NodeRuntime instance. This is provided by the runtime.runInNodeMode() function.
  • report: A Report object generated by runtime.report().
  • fn: A function that converts the inner ReportResponse to a Request or RequestJson object.

Returns:

An object with a .result() method that blocks until the HTTP request completes and returns the Response.

Helper Functions

The SDK provides utility functions to simplify working with HTTP responses. These functions are exported from @chainlink/cre-sdk and can be used to check status codes, decode text, and parse JSON responses.

ok()

Checks if an HTTP response indicates success (status code 200-299).

Signature:

ok(response: Response): boolean

Parameters:

  • response: The HTTP response object.

Returns:

true if the status code is in the 200-299 range, false otherwise.

Example:

import { ok } from "@chainlink/cre-sdk"

const response = sendRequester.sendRequest({ url: apiUrl }).result()

if (!ok(response)) {
  throw new Error(`HTTP request failed with status: ${response.statusCode}`)
}

text()

Decodes the response body as UTF-8 text and automatically trims whitespace.

Signature:

text(response: Response): string

Parameters:

  • response: The HTTP response object.

Returns:

The response body decoded as a UTF-8 string with leading and trailing whitespace removed.

Example:

import { text } from "@chainlink/cre-sdk"

const response = sendRequester.sendRequest({ url: apiUrl }).result()
const responseText = text(response) // Automatically trimmed

json()

Parses the response body as JSON.

Signature:

json(response: Response): unknown

Parameters:

  • response: The HTTP response object.

Returns:

The parsed JSON object (type unknown, should be cast to your expected type).

Example:

import { json } from "@chainlink/cre-sdk"

interface ApiResponse {
  price: number
  timestamp: number
}

const response = sendRequester.sendRequest({ url: apiUrl }).result()
const data = json(response) as ApiResponse

getHeader()

Retrieves a specific HTTP header value from the response (case-insensitive).

Signature:

getHeader(response: Response, name: string): string | undefined

Parameters:

  • response: The HTTP response object.
  • name: The header name to retrieve (case-insensitive).

Returns:

The header value as a string, or undefined if the header is not found.

Example:

import { getHeader } from "@chainlink/cre-sdk"

const response = sendRequester.sendRequest({ url: apiUrl }).result()

const contentType = getHeader(response, "content-type")
const rateLimit = getHeader(response, "X-Rate-Limit-Remaining")

Using all helpers together:

import { ok, json, getHeader } from "@chainlink/cre-sdk"

const fetchPrice = (sendRequester: HTTPSendRequester, url: string): number => {
  const response = sendRequester.sendRequest({ url }).result()

  if (!ok(response)) {
    throw new Error(`HTTP request failed with status: ${response.statusCode}`)
  }

  // Check content type
  const contentType = getHeader(response, "content-type")
  if (!contentType?.includes("application/json")) {
    throw new Error("Expected JSON response")
  }

  const data = json(response) as { price: number }
  return data.price
}

Associated Types

Request / RequestJson

Defines the parameters for an outgoing HTTP request.

FieldTypeDescription
urlstringThe URL of the API endpoint.
methodstring (optional)The HTTP method (e.g., "GET", "POST"). Defaults to "GET".
headers{ [key: string]: string } (optional)Optional HTTP headers.
bodystring (base64-encoded) (optional)Optional raw request body (must be base64-encoded).
timeoutMsnumber (optional)Optional request timeout in milliseconds.
cacheSettingsCacheSettings | CacheSettingsJson (optional)Optional caching behavior for the request.

CacheSettings / CacheSettingsJson

Defines caching behavior for the request. This is particularly useful for preventing duplicate execution of non-idempotent requests (POST, PUT, PATCH, DELETE).

Field
Type
Description
readFromCachebooleanIf true, attempt to read a cached response for the request. When combined with a non-zero maxAgeMs, this enables cache reading. If false, the request will not read from cache but may still store a response if enabled.
maxAgeMsnumberMaximum age of a cached response in milliseconds that this workflow will accept. If 0 or not set, the request will not attempt to read from cache. Max value is 600000 ms (10 minutes).

Understanding CacheSettings behavior

When you make HTTP requests in CRE, all nodes in the DON execute the request by default. For read-only operations (like GET), this is fine—consensus ensures a reliable result. However, for non-idempotent operations (POST, PUT, PATCH, DELETE), multiple executions can cause problems:

  • Creating duplicate resources (e.g., multiple user accounts)
  • Triggering duplicate actions (e.g., sending multiple emails)
  • Unintended side effects (e.g., incrementing counters multiple times)

CacheSettings provides a solution by enabling a shared cache across all nodes in the DON:

  1. Node 1 makes the HTTP request and stores the response in the shared cache
  2. Nodes 2, 3, etc. check the cache first and reuse the cached response if it exists

Important considerations:

  • Best effort mechanism: The caching works reliably in most scenarios, but is not guaranteed to prevent all duplicates. For example, gateway availability (network issues or deployments) can affect routing to different gateway instances.
  • Request matching: Caching only works when all nodes construct identical requests (same URL, headers, and body). Ensure your workflow generates deterministic request payloads.
  • Understanding maxAgeMs: This controls how stale your workflow will accept a cached response to be:
    • The cache system stores responses for up to 10 minutes (600000 ms) by default (system-wide TTL)
    • maxAgeMs lets your workflow specify: "I'll only use cached data if it's fresher than X milliseconds"
    • Setting maxAgeMs to 0 or not providing it forces a fresh fetch every time (but still stores if readFromCache is true)
    • For POST/PUT/PATCH/DELETE operations: Set this slightly longer than your workflow's expected execution time (e.g., 60000 for 60 seconds)
    • For GET operations where you want to reuse data: Set this to your desired cache duration

Example with caching:

const bodyBytes = new TextEncoder().encode(JSON.stringify({ name: "Resource" }))
const body = Buffer.from(bodyBytes).toString("base64")

const response = sendRequester
  .sendRequest({
    url: "https://api.example.com/create-resource",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body,
    cacheSettings: {
      readFromCache: true,
      maxAgeMs: 60000, // Accept cached responses up to 60 seconds old
    },
  })
  .result()

For practical examples, see the POST request guide.

Response

The result of the HTTP call from a single node (before consensus aggregation).

FieldTypeDescription
statusCodenumberThe HTTP status code.
headers{ [key: string]: string }The HTTP response headers.
bodyUint8Array | string (base64)The raw response body.

Example parsing response body:

import { text, json } from "@chainlink/cre-sdk"

const response = sendRequester.sendRequest({ url: apiUrl }).result()

// Parse as text using helper
const responseText = text(response)

// Parse as JSON using helper
const data = json(response)

// Or manually with TextDecoder
const manualText = new TextDecoder().decode(response.body)
const manualData = JSON.parse(manualText)

Usage Patterns

Simple GET request

import { cre, consensusMedianAggregation, ok, json, type HTTPSendRequester } from "@chainlink/cre-sdk"

const fetchData = (sendRequester: HTTPSendRequester, url: string): number => {
  const response = sendRequester.sendRequest({ url }).result()

  if (!ok(response)) {
    throw new Error(`HTTP request failed with status: ${response.statusCode}`)
  }

  const data = json(response) as { value: number }
  return data.value
}

// In your workflow
const httpClient = new cre.capabilities.HTTPClient()
const result = httpClient.sendRequest(runtime, fetchData, consensusMedianAggregation<number>())(apiUrl).result()

POST request with caching

import { cre, consensusIdenticalAggregation, ok, json, type HTTPSendRequester } from "@chainlink/cre-sdk"

const createResource = (sendRequester: HTTPSendRequester, payload: { name: string }): { id: string } => {
  // Encode the body as base64
  const bodyBytes = new TextEncoder().encode(JSON.stringify(payload))
  const body = Buffer.from(bodyBytes).toString("base64")

  const response = sendRequester
    .sendRequest({
      url: "https://api.example.com/resources",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body,
      cacheSettings: {
        readFromCache: true,
        maxAgeMs: 60000, // 60 seconds
      },
    })
    .result()

  if (!ok(response)) {
    throw new Error(`Failed to create resource: ${response.statusCode}`)
  }

  const data = json(response) as { id: string }
  return { id: data.id }
}

// In your workflow
const httpClient = new cre.capabilities.HTTPClient()
const resource = httpClient
  .sendRequest(runtime, createResource, consensusIdenticalAggregation<{ id: string }>())({ name: "My Resource" })
  .result()

Complex object aggregation

For complex objects with multiple fields, use ConsensusAggregationByFields():

import {
  cre,
  ConsensusAggregationByFields,
  median,
  identical,
  ok,
  json,
  type HTTPSendRequester,
} from "@chainlink/cre-sdk"

interface ReserveInfo {
  lastUpdated: Date
  totalReserve: number
  status: string
}

const fetchReserveInfo = (sendRequester: HTTPSendRequester, url: string): ReserveInfo => {
  const response = sendRequester.sendRequest({ url }).result()

  if (!ok(response)) {
    throw new Error(`HTTP request failed with status: ${response.statusCode}`)
  }

  const data = json(response) as { timestamp: number; reserve: number; status: string }

  return {
    lastUpdated: new Date(data.timestamp),
    totalReserve: data.reserve,
    status: data.status,
  }
}

// In your workflow
const httpClient = new cre.capabilities.HTTPClient()
const reserveInfo = httpClient
  .sendRequest(
    runtime,
    fetchReserveInfo,
    ConsensusAggregationByFields<ReserveInfo>({
      lastUpdated: median,
      totalReserve: median,
      status: identical,
    })
  )(apiUrl)
  .result()

Get the latest Chainlink content straight to your inbox.