# SDK Reference: HTTP Client
Source: https://docs.chain.link/cre/reference/sdk/http-client-ts
Last Updated: 2026-01-26

> For the complete documentation index, see [llms.txt](/llms.txt).

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)](#high-level-sendrequest-recommended):** Automatically wraps your request in the `runtime.runInNodeMode()` pattern with consensus aggregation.
- **[Low-level](#low-level-sendrequest):** Requires manual wrapping in a `runtime.runInNodeMode()` block for complex scenarios.

For complete step-by-step examples, see the [GET requests](/cre/guides/workflow/using-http-client/get-request) and [POST requests](/cre/guides/workflow/using-http-client/post-request) guides.

## High-level `sendRequest()` (recommended)

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:**

```typescript
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](/cre/reference/sdk/consensus) (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:**

```typescript
import { HTTPClient, 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 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:**

```typescript
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:**

```typescript
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`.

> **NOTE: Caching limitation**
>
> Note that caching is limited for `sendReport()` as reports may contain different sets of signatures on different
> nodes, leading to cache misses.

**Example:**

```typescript
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:**

```typescript
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:**

```typescript
import { HTTPClient, 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 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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
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.

> **CAUTION: Redirects are not supported**
>
> HTTP requests to URLs that return redirects (3xx status codes) will fail. Ensure the URL you provide is the final destination and does not redirect to another URL.

| Field           | Type                                              | Description                                                                                                             |
| --------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `url`           | `string`                                          | The URL of the API endpoint.                                                                                            |
| `method`        | `string` (optional)                               | The HTTP method (e.g., `"GET"`, `"POST"`). Defaults to `"GET"`.                                                         |
| `headers`       | `{ [key: string]: string }` (optional)            | Optional HTTP headers.                                                                                                  |
| `body`          | `string` (base64-encoded) (optional)              | Optional raw request body (must be base64-encoded).                                                                     |
| `timeout`       | `string` (optional)                               | Request timeout as a duration string (e.g., `"5s"`, `"8s"`). See [Request Timeout](#request-timeout) below for details. |
| `cacheSettings` | `CacheSettings` \| `CacheSettingsJson` (optional) | Optional caching behavior for the request.                                                                              |

### Request Timeout

The `timeout` field specifies how long to wait for an HTTP request to complete before cancelling it.

**Format:**

- Specify timeout as a string ending with `"s"` (seconds)
- Examples: `"5s"` (5 seconds), `"8s"` (8 seconds), `"3.5s"` (3.5 seconds)

> **NOTE: Protocol Buffers Duration**
>
> Under the hood, `timeout` uses the [Protocol Buffers Duration type](https://protobuf.dev/reference/protobuf/google.protobuf/#duration). In JSON format, it's encoded as a string ending with "s", where fractional seconds can represent nanosecond precision if needed.

**Default and Limits:**

- **Default**: If not specified, a default timeout of **5 seconds** is applied
- **Maximum**: Default maximum is **10 seconds**. Requests exceeding this limit will error

**Example:**

```typescript
type Config = {
  apiUrl: string
  requestTimeout: string
}

const httpClient = new HTTPClient()

const req = {
  method: "GET",
  url: config.apiUrl,
  timeout: config.requestTimeout, // e.g., "8s"
}

const response = httpClient.sendRequest(nodeRuntime, req).result()
```

**Example configuration:**

```yaml
# config.yaml
apiUrl: "https://api.example.com/data"
requestTimeout: "8s" # Max: 10s
```

### `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                                                                                                                                                                                                                     |
| --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `readFromCache` | `boolean` | If `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. |
| `maxAgeMs`      | `number`  | Maximum 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:**

```typescript
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](/cre/guides/workflow/using-http-client/post-request).

### `Response`

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

| Field        | Type                              | Description                |
| ------------ | --------------------------------- | -------------------------- |
| `statusCode` | `number`                          | The HTTP status code.      |
| `headers`    | `{ [key: string]: string }`       | The HTTP response headers. |
| `body`       | `Uint8Array` \| `string` (base64) | The raw response body.     |

**Example parsing response body:**

```typescript
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

```typescript
import { HTTPClient, 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 HTTPClient()
const result = httpClient.sendRequest(runtime, fetchData, consensusMedianAggregation<number>())(apiUrl).result()
```

### POST request with caching

```typescript
import { HTTPClient, 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 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()`:

```typescript
import {
  HTTPClient,
  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 HTTPClient()
const reserveInfo = httpClient
  .sendRequest(
    runtime,
    fetchReserveInfo,
    ConsensusAggregationByFields<ReserveInfo>({
      lastUpdated: median,
      totalReserve: median,
      status: identical,
    })
  )(apiUrl)
  .result()
```