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.
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:
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-levelRuntimefrom your workflow callback.fn: A function containing your core fetching and parsing logic. It receives aSendRequesterinstance 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: ARequestorRequestJsonobject 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: AReportobject generated byruntime.report().fn: A function that converts the innerReportResponseto aRequestorRequestJsonobject.
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: ANodeRuntimeinstance. This is provided by theruntime.runInNodeMode()function.input: ARequestorRequestJsonobject 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: ANodeRuntimeinstance. This is provided by theruntime.runInNodeMode()function.report: AReportobject generated byruntime.report().fn: A function that converts the innerReportResponseto aRequestorRequestJsonobject.
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.
| 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). |
timeoutMs | number (optional) | Optional request timeout in milliseconds. |
cacheSettings | CacheSettings | 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 |
|---|---|---|
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:
- Node 1 makes the HTTP request and stores the response in the shared cache
- 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)
maxAgeMslets your workflow specify: "I'll only use cached data if it's fresher than X milliseconds"- Setting
maxAgeMsto0or not providing it forces a fresh fetch every time (but still stores ifreadFromCacheis true) - For POST/PUT/PATCH/DELETE operations: Set this slightly longer than your workflow's expected execution time (e.g.,
60000for 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).
| 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:
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()