Making Confidential Requests

The ConfidentialHTTPClient is the SDK's interface for the underlying Confidential HTTP Capability. It allows your workflow to make privacy-preserving API calls where secrets are injected inside a secure enclave and responses can be optionally encrypted.

Unlike the regular HTTPClient, the Confidential HTTP client:

  • Executes the request in a secure enclave (not on each node individually)
  • Injects secrets from the Vault DON using template syntax
  • Optionally encrypts the response before returning it to your workflow

Prerequisites

This guide assumes you have:

Step-by-step example

This example shows a workflow that makes a confidential GET request to an API, injecting a secret into the request headers using template syntax.

Step 1: Configure your workflow

Add the API URL to your config.json file.

{
  "schedule": "0 */5 * * * *",
  "url": "https://api.example.com/data",
  "owner": ""
}

Step 2: Set up secrets for simulation

Confidential HTTP uses the secrets.yaml file. If you've already set up secrets for your project, you can reuse the same file. For a full walkthrough, see Using Secrets in Simulation.

Add the secrets your confidential request needs to your secrets.yaml:

# secrets.yaml
secretsNames:
  myApiKey:
    - MY_API_KEY_ALL

Provide the actual value via an environment variable or .env file:

export MY_API_KEY_ALL="your-secret-api-key"

Step 3: Define your types

import { z } from "zod"

const configSchema = z.object({
  schedule: z.string(),
  url: z.string(),
  owner: z.string(),
})

type Config = z.infer<typeof configSchema>

type TransactionResult = {
  transactionId: string
  status: string
}

Step 4: Implement the fetch function

Create the function that will be passed to sendRequest(). This function receives a ConfidentialHTTPSendRequester and your config as parameters:

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

const fetchTransaction = (sendRequester: ConfidentialHTTPSendRequester, config: Config): TransactionResult => {
  // 1. Send the confidential request
  const response = sendRequester
    .sendRequest({
      request: {
        url: config.url,
        method: "GET",
        multiHeaders: {
          Authorization: { values: ["Basic {{.myApiKey}}"] },
        },
      },
      vaultDonSecrets: [{ key: "myApiKey", owner: config.owner }],
    })
    .result()

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

  // 3. Parse and return the result
  return json(response) as TransactionResult
}

Step 5: Wire it into your workflow

In your trigger handler, call confHTTPClient.sendRequest() with your fetch function and a consensus method:

import {
  CronCapability,
  ConfidentialHTTPClient,
  handler,
  consensusIdenticalAggregation,
  type Runtime,
  Runner,
} from "@chainlink/cre-sdk"

const onCronTrigger = (runtime: Runtime<Config>): string => {
  const confHTTPClient = new ConfidentialHTTPClient()

  const result = confHTTPClient
    .sendRequest(runtime, fetchTransaction, consensusIdenticalAggregation<TransactionResult>())(runtime.config)
    .result()

  runtime.log(`Transaction result: ${result.transactionId}${result.status}`)
  return result.transactionId
}

Step 6: Simulate

Run the simulation:

cre workflow simulate

Template syntax for secrets

Secrets are injected into request bodies and headers using Go template syntax: {{.secretName}}. The placeholder name must match the key in your vaultDonSecrets list.

In the request body:

{ "auth": "{{.myApiKey}}", "data": "public-data" }

In headers:

multiHeaders: {
  "Authorization": { values: ["Basic {{.myCredential}}"] },
}

The template placeholders are resolved inside the enclave. The actual secret values never appear in your workflow code or in node memory.

Response encryption

By default, the API response is returned unencrypted (encryptOutput: false). To encrypt the response body before it leaves the enclave, set encryptOutput: true and provide an AES-256 encryption key as a Vault DON secret.

Setting up response encryption

  1. Store an AES-256 key as a Vault DON secret with the identifier san_marino_aes_gcm_encryption_key:

    # secrets.yaml
    secretsNames:
      san_marino_aes_gcm_encryption_key:
        - AES_KEY_ALL
    

The key must be a 256-bit (32 bytes) hex-encoded string:

export AES_KEY_ALL="your-256-bit-hex-encoded-key"
  1. Include the key in your vaultDonSecrets and set encryptOutput: true:

    const response = sendRequester
      .sendRequest({
        request: {
          url: config.url,
          method: "GET",
          multiHeaders: {
            Authorization: { values: ["Basic {{.myApiKey}}"] },
          },
        },
        vaultDonSecrets: [{ key: "myApiKey", owner: config.owner }, { key: "san_marino_aes_gcm_encryption_key" }],
        encryptOutput: true,
      })
      .result()
    
  2. Decrypt the response in your own backend service. The encrypted response body is structured as nonce || ciphertext || tag and uses AES-GCM encryption.

Response helper functions

The SDK response helpers ok(), text(), and json() work with Confidential HTTP responses just as they do with regular HTTP responses. For full documentation, see the HTTP Client SDK Reference.

Complete example

Here's the full workflow code for a confidential HTTP request with secret injection:

import {
  CronCapability,
  ConfidentialHTTPClient,
  handler,
  consensusIdenticalAggregation,
  ok,
  json,
  type ConfidentialHTTPSendRequester,
  type Runtime,
  Runner,
} from "@chainlink/cre-sdk"
import { z } from "zod"

// Config schema
const configSchema = z.object({
  schedule: z.string(),
  url: z.string(),
  owner: z.string(),
})

type Config = z.infer<typeof configSchema>

// Result type
type TransactionResult = {
  transactionId: string
  status: string
}

// Fetch function — receives a ConfidentialHTTPSendRequester and config
const fetchTransaction = (sendRequester: ConfidentialHTTPSendRequester, config: Config): TransactionResult => {
  const response = sendRequester
    .sendRequest({
      request: {
        url: config.url,
        method: "GET",
        multiHeaders: {
          Authorization: { values: ["Basic {{.myApiKey}}"] },
        },
      },
      vaultDonSecrets: [{ key: "myApiKey", owner: config.owner }],
    })
    .result()

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

  return json(response) as TransactionResult
}

// Main workflow handler
const onCronTrigger = (runtime: Runtime<Config>): string => {
  const confHTTPClient = new ConfidentialHTTPClient()

  const result = confHTTPClient
    .sendRequest(runtime, fetchTransaction, consensusIdenticalAggregation<TransactionResult>())(runtime.config)
    .result()

  runtime.log(`Transaction result: ${result.transactionId}${result.status}`)
  return result.transactionId
}

// Initialize workflow
const initWorkflow = (config: Config) => {
  return [
    handler(
      new CronCapability().trigger({
        schedule: config.schedule,
      }),
      onCronTrigger
    ),
  ]
}

export async function main() {
  const runner = await Runner.newRunner<Config>({ configSchema })
  await runner.run(initWorkflow)
}

API reference

For the full list of types and methods available on the Confidential HTTP client, see the Confidential HTTP Client SDK Reference.

Get the latest Chainlink content straight to your inbox.