Making Confidential Requests

confidentialhttp.Client implements the Confidential HTTP capability. Use it when an outbound call should carry sensitive credentials or request fields without assembling them as plain strings in workflow code on every node—see when to use Confidential vs. regular HTTP. For those values, use VaultDonSecrets with {{.key}} placeholders only; runtime.GetSecret() in headers or body follows a different trust boundary.

Unlike the regular http.Client, the Confidential HTTP client:

  • Executes the request in a secure enclave (not on each node individually)
  • Resolves VaultDonSecrets into the request via Vault DON 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 POST request to an API, injecting an API secret into both the request body and 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"
}

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: Build the confidential request

The key difference from regular HTTP is how you construct the request. You provide:

  • An HTTPRequest with template placeholders ({{.secretName}}) in the body and/or headers
  • A list of VaultDonSecrets identifying which secrets to fetch from the Vault DON
  • An optional EncryptOutput flag to encrypt the response
import (
	"github.com/smartcontractkit/cre-sdk-go/capabilities/networking/confidentialhttp"
	"github.com/smartcontractkit/cre-sdk-go/cre"
)

type Config struct {
	Schedule string `json:"schedule"`
	URL      string `json:"url"`
}

type Result struct {
	TransactionID string `json:"transactionId" consensus:"identical"`
	Status        string `json:"status"        consensus:"identical"`
}

Step 4: Implement the request logic

Unlike the regular HTTP client, the Confidential HTTP client takes a cre.Runtime directly and does not require cre.RunInNodeMode wrapping:

func makeConfidentialRequest(config Config, runtime cre.Runtime) (Result, error) {
	// 1. Define the request body with secret template placeholders
	payload := `{"auth": "{{.myApiKey}}", "action": "getTransaction", "id": "tx-123"}`

	// 2. Define headers with secret template placeholders
	headers := map[string]*confidentialhttp.HeaderValues{
		"Content-Type": {
			Values: []string{"application/json"},
		},
		"Authorization": {
			Values: []string{"Basic {{.myApiKey}}"},
		},
	}

	// 3. Create the client and send the request
	client := confidentialhttp.Client{}
	resp, err := client.SendRequest(runtime, &confidentialhttp.ConfidentialHTTPRequest{
		Request: &confidentialhttp.HTTPRequest{
			Url:          config.URL,
			Method:       "POST",
			Body:         &confidentialhttp.HTTPRequest_BodyString{BodyString: payload},
			MultiHeaders: headers,
			EncryptOutput: false,
		},
		VaultDonSecrets: []*confidentialhttp.SecretIdentifier{
			{Key: "myApiKey"},
		},
	}).Await()
	if err != nil {
		return Result{}, fmt.Errorf("confidential HTTP request failed: %w", err)
	}

	// 4. Parse the response
	var result Result
	if err := json.Unmarshal(resp.Body, &result); err != nil {
		return Result{}, fmt.Errorf("failed to parse response: %w", err)
	}

	return result, nil
}

Step 5: Wire it into your workflow

Call the request function from your trigger handler:

func onCronTrigger(config *Config, runtime cre.Runtime, outputs *cron.Payload) (string, error) {
	result, err := makeConfidentialRequest(*config, runtime)
	if err != nil {
		return "", fmt.Errorf("failed to get result: %w", err)
	}

	runtime.Logger().Info("Transaction result", "id", result.TransactionID, "status", result.Status)
	return result.TransactionID, nil
}

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:

headers := map[string]*confidentialhttp.HeaderValues{
	"Authorization": {
		Values: []string{"Basic {{.myCredential}}"},
	},
}

The template placeholders are resolved inside the enclave. The actual secret values never appear in your workflow code or in node memory. Credentials must be wired through VaultDonSecrets as in Step 4—not interpolated from runtime.GetSecret().

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"
    
  2. Include the key in your VaultDonSecrets and set EncryptOutput: true:

    resp, err := client.SendRequest(runtime, &confidentialhttp.ConfidentialHTTPRequest{
    	Request: &confidentialhttp.HTTPRequest{
    		Url:           config.URL,
    		Method:        "POST",
    		Body:          &confidentialhttp.HTTPRequest_BodyString{BodyString: payload},
    		MultiHeaders:  headers,
    		EncryptOutput: true,
    	},
    	VaultDonSecrets: []*confidentialhttp.SecretIdentifier{
    		{Key: "myApiKey"},
    		{Key: "san_marino_aes_gcm_encryption_key"},
    	},
    }).Await()
    
  3. Decrypt the response in your own secure backend using AES-GCM. The encrypted response body is structured as nonce || ciphertext || tag:

    import (
    	"crypto/aes"
    	"crypto/cipher"
    	"encoding/hex"
    )
    
    func AESGCMDecrypt(blob []byte, key []byte) ([]byte, error) {
    	block, err := aes.NewCipher(key)
    	if err != nil {
    		return nil, err
    	}
    
    	gcm, err := cipher.NewGCM(block)
    	if err != nil {
    		return nil, err
    	}
    
    	nonceSize := gcm.NonceSize()
    	if len(blob) < nonceSize {
    		return nil, fmt.Errorf("ciphertext too short")
    	}
    
    	nonce, ciphertext := blob[:nonceSize], blob[nonceSize:]
    	return gcm.Open(nil, nonce, ciphertext, nil)
    }
    

Complete example

Here's the full workflow code for a confidential HTTP request with response encryption:

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"

	"github.com/smartcontractkit/cre-sdk-go/capabilities/networking/confidentialhttp"
	"github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
	"github.com/smartcontractkit/cre-sdk-go/cre"
)

type Config struct {
	Schedule string `json:"schedule"`
	URL      string `json:"url"`
}

type Result struct {
	TransactionID string `json:"transactionId" consensus:"identical"`
	Status        string `json:"status"        consensus:"identical"`
}

func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
	cronTriggerCfg := &cron.Config{
		Schedule: config.Schedule,
	}

	workflow := cre.Workflow[*Config]{
		cre.Handler(
			cron.Trigger(cronTriggerCfg),
			onCronTrigger,
		),
	}

	return workflow, nil
}

func onCronTrigger(config *Config, runtime cre.Runtime, outputs *cron.Payload) (string, error) {
	result, err := makeConfidentialRequest(*config, runtime)
	if err != nil {
		return "", fmt.Errorf("failed to get result: %w", err)
	}

	runtime.Logger().Info("Transaction result", "id", result.TransactionID, "status", result.Status)
	return result.TransactionID, nil
}

func makeConfidentialRequest(config Config, runtime cre.Runtime) (Result, error) {
	payload := `{"auth": "{{.myApiKey}}", "action": "getTransaction", "id": "tx-123"}`

	headers := map[string]*confidentialhttp.HeaderValues{
		"Content-Type": {
			Values: []string{"application/json"},
		},
		"Authorization": {
			Values: []string{"Basic {{.myApiKey}}"},
		},
	}

	client := confidentialhttp.Client{}
	resp, err := client.SendRequest(runtime, &confidentialhttp.ConfidentialHTTPRequest{
		Request: &confidentialhttp.HTTPRequest{
			Url:           config.URL,
			Method:        "POST",
			Body:          &confidentialhttp.HTTPRequest_BodyString{BodyString: payload},
			MultiHeaders:  headers,
			EncryptOutput: true,
		},
		VaultDonSecrets: []*confidentialhttp.SecretIdentifier{
			{Key: "myApiKey"},
			{Key: "san_marino_aes_gcm_encryption_key"},
		},
	}).Await()
	if err != nil {
		return Result{}, fmt.Errorf("confidential HTTP request failed: %w", err)
	}

	// In a real workflow, you would forward the encrypted body to your
	// secure backend for decryption. This inline decryption is shown
	// for demonstration purposes only.
	keyHex := os.Getenv("AES_KEY_ALL") // your 256-bit hex-encoded key
	keyBytes, _ := hex.DecodeString(keyHex)
	decrypted, err := AESGCMDecrypt(resp.Body, keyBytes)
	if err != nil {
		return Result{}, fmt.Errorf("failed to decrypt response: %w", err)
	}

	var result Result
	if err := json.Unmarshal(decrypted, &result); err != nil {
		return Result{}, fmt.Errorf("failed to parse response: %w", err)
	}

	return result, nil
}

func AESGCMDecrypt(blob []byte, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonceSize := gcm.NonceSize()
	if len(blob) < nonceSize {
		return nil, fmt.Errorf("ciphertext too short")
	}

	nonce, ciphertext := blob[:nonceSize], blob[nonceSize:]
	return gcm.Open(nil, nonce, ciphertext, nil)
}

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.