# Submitting Reports via HTTP
Source: https://docs.chain.link/cre/guides/workflow/using-http-client/submitting-reports-http-ts
Last Updated: 2026-01-20

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

This guide shows how to send a cryptographically signed report (generated by your workflow) to an external HTTP API. You'll learn how to write a transformation function that formats the report for your specific API's requirements.

**What you'll learn:**

- How to use `sendReport()` to submit reports via HTTP
- How to write transformation functions for different API formats
- Best practices for report submission and deduplication

> **NOTE: Need onchain submission instead?**
>
> This guide covers HTTP submission. For submitting reports to smart contracts, see [Submitting Reports Onchain](/cre/guides/workflow/using-evm-client/onchain-write/overview-ts).

## Prerequisites

- Familiarity with [making POST requests](/cre/guides/workflow/using-http-client/post-request)
- Familiarity with `runtime.report()` (covered [below](#generating-reports-for-http-submission))

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

## Quick start: Minimal example

Here's the simplest possible workflow that generates and submits a report via HTTP:

```typescript
import { ok, type ReportResponse, type RequestJson, type HTTPSendRequester } from "@chainlink/cre-sdk"

const formatReportSimple = (r: ReportResponse): RequestJson => {
  return {
    url: "https://api.example.com/reports",
    method: "POST",
    body: Buffer.from(r.rawReport).toString("base64"), // Send the raw report bytes (base64-encoded)
    headers: {
      "Content-Type": "application/octet-stream",
    },
    cacheSettings: {
      readFromCache: true,
      maxAgeMs: 60000, // Accept cached responses up to 60 seconds old
    },
  }
}

const submitReport = (sendRequester: HTTPSendRequester, report: Report): { success: boolean } => {
  const response = sendRequester.sendReport(report, formatReportSimple).result()

  if (!ok(response)) {
    throw new Error(`API returned error: status=${response.statusCode}`)
  }

  return { success: true }
}
```

**What's happening here:**

1. `formatReportSimple` transforms the report into an HTTP request that your API understands
2. `sendRequester.sendReport()` calls your transformation function and sends the request
3. The SDK handles consensus and returns the result

The rest of this guide explains how this works and shows different formatting patterns for various API requirements.

## How it works

### The report structure

When you call `runtime.report()`, the SDK creates a `ReportResponse` containing:

```typescript
interface ReportResponse {
  rawReport: Uint8Array // Your encoded data + metadata
  reportContext: Uint8Array // Workflow execution context
  sigs: AttributedSignature[] // Cryptographic signatures from DON nodes
  configDigest: Uint8Array // DON configuration identifier
  seqNr: bigint // Sequence number
}
```

This structure contains everything your API might need:

- **`rawReport`**: The actual report data (always required)
- **`sigs`**: Cryptographic signatures from DON nodes (for verification)
- **`reportContext`**: Metadata about the workflow execution
- **`seqNr`**: Sequence number

### The transformation function

Your transformation function tells the SDK how to format the report for your API:

```typescript
;(reportResponse: ReportResponse) => RequestJson
```

**The SDK calls this function internally:**

1. You pass your transformation function to `sendReport()`
2. The SDK calls it with the generated `ReportResponse`
3. Your function returns a `RequestJson` formatted for your API
4. The SDK sends the request and handles consensus

**Why is this needed?** Different APIs expect different formats:

- Some want raw binary data
- Some want JSON with base64-encoded fields
- Some want signatures in headers, others in the body

The transformation function gives you complete control over the format.

## Formatting patterns

Here are common patterns for formatting reports. Choose the one that matches your API's requirements.

### Choosing the right pattern

| Pattern                                                                                                 | When to use                                               |
| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| [**Pattern 1: Report in body**](#pattern-1-report-in-body-simplest)                                     | Your API accepts raw binary data and handles decoding     |
| [**Pattern 2: Report + signatures in body**](#pattern-2-report--signatures-in-body)                     | Your API needs everything concatenated in one binary blob |
| [**Pattern 3: Report in body, signatures in headers**](#pattern-3-report-in-body-signatures-in-headers) | Your API needs signatures separated for easier parsing    |
| [**Pattern 4: JSON-formatted report**](#pattern-4-json-formatted-report)                                | Your API only accepts JSON payloads                       |

### Pattern 1: Report in body (simplest)

Use this when your API accepts raw binary data:

```typescript
import type { ReportResponse, RequestJson } from "@chainlink/cre-sdk"

const formatReportSimple = (r: ReportResponse): RequestJson => {
  return {
    url: "https://api.example.com/reports",
    method: "POST",
    body: Buffer.from(r.rawReport).toString("base64"), // Just send the report (base64-encoded)
    headers: {
      "Content-Type": "application/octet-stream",
    },
    cacheSettings: {
      readFromCache: true, // Enable caching
      maxAgeMs: 60000, // Accept cached responses up to 60 seconds old
    },
  }
}
```

### Pattern 2: Report + signatures in body

Use this when your API needs everything concatenated in one payload:

```typescript
const formatReportWithSignatures = (r: ReportResponse): RequestJson => {
  // Concatenate report, context, and all signatures
  const reportBytes = new Uint8Array(r.rawReport)
  const contextBytes = new Uint8Array(r.reportContext)

  let totalLength = reportBytes.length + contextBytes.length
  for (const sig of r.sigs) {
    totalLength += sig.signature.length
  }

  const body = new Uint8Array(totalLength)
  let offset = 0

  // Copy report
  body.set(reportBytes, offset)
  offset += reportBytes.length

  // Copy context
  body.set(contextBytes, offset)
  offset += contextBytes.length

  // Copy all signatures
  for (const sig of r.sigs) {
    body.set(new Uint8Array(sig.signature), offset)
    offset += sig.signature.length
  }

  return {
    url: "https://api.example.com/reports",
    method: "POST",
    body: Buffer.from(body).toString("base64"),
    headers: {
      "Content-Type": "application/octet-stream",
    },
    cacheSettings: {
      readFromCache: true,
      maxAgeMs: 60000,
    },
  }
}
```

### Pattern 3: Report in body, signatures in headers

Use this when your API needs signatures separated for easier parsing:

```typescript
const formatReportWithHeaderSigs = (r: ReportResponse): RequestJson => {
  const headers: { [key: string]: string } = {
    "Content-Type": "application/octet-stream",
  }

  // Add signatures to headers
  r.sigs.forEach((sig, i) => {
    const sigKey = `X-Signature-${i}`
    const signerKey = `X-Signer-ID-${i}`

    headers[sigKey] = Buffer.from(sig.signature).toString("base64")
    headers[signerKey] = sig.signerId.toString()
  })

  return {
    url: "https://api.example.com/reports",
    method: "POST",
    body: Buffer.from(r.rawReport).toString("base64"),
    headers,
    cacheSettings: {
      readFromCache: true,
      maxAgeMs: 60000,
    },
  }
}
```

### Pattern 4: JSON-formatted report

Use this when your API only accepts JSON payloads:

```typescript
interface ReportPayload {
  report: string
  context: string
  signatures: string[]
  configDigest: string
  seqNr: string
}

const formatReportAsJSON = (r: ReportResponse): RequestJson => {
  // Extract signatures
  const sigs = r.sigs.map((sig) => Buffer.from(sig.signature).toString("base64"))

  // Create JSON payload
  const payload: ReportPayload = {
    report: Buffer.from(r.rawReport).toString("base64"),
    context: Buffer.from(r.reportContext).toString("base64"),
    signatures: sigs,
    configDigest: Buffer.from(r.configDigest).toString("base64"),
    seqNr: r.seqNr.toString(),
  }

  const bodyBytes = new TextEncoder().encode(JSON.stringify(payload))

  return {
    url: "https://api.example.com/reports",
    method: "POST",
    body: Buffer.from(bodyBytes).toString("base64"),
    headers: {
      "Content-Type": "application/json",
    },
    cacheSettings: {
      readFromCache: true,
      maxAgeMs: 60000,
    },
  }
}
```

### Understanding `cacheSettings` for reports

You'll notice that all the patterns above include `cacheSettings`. This is critical for report submissions, just like it is for [POST requests](/cre/guides/workflow/using-http-client/post-request).

For a complete explanation of how `cacheSettings` works in general, see [Understanding `CacheSettings` behavior](/cre/reference/sdk/http-client-ts#understanding-cachesettings-behavior) in the HTTP Client reference.

**Why use `cacheSettings`?**

When a workflow executes, **all nodes in the DON** attempt to send the report to your API. Without caching, your API would receive multiple identical submissions (one from each node). `cacheSettings` prevents this by having the first node cache the response, which other nodes can reuse.

**Why are cache hits limited for reports?**

Unlike regular POST requests where caching can be very effective, **reports have a more limited cache effectiveness** due to signature variance:

1. Each DON node generates its own **unique cryptographic signature** for the report
2. These signatures are part of the `ReportResponse` structure
3. When nodes construct the HTTP request body (whether concatenating signatures or including them in headers), the signatures differ

**In practice:** Even though cache hits are limited, you should still include `cacheSettings` to prevent worst-case scenarios where all nodes hit your API simultaneously.

**The real solution: API-side deduplication**

Because caching alone cannot prevent all duplicate submissions, your receiving API **must implement its own deduplication logic**:

- Use the **hash of the report** (`keccak256(rawReport)`) as the unique identifier
- Store this hash when processing a report
- Reject any subsequent submissions with the same hash

This approach is reliable because the `rawReport` is identical across all nodes—only the signatures vary.

## Generating reports for HTTP submission

Before you can submit a report via HTTP, you need to generate it using `runtime.report()`. This creates a cryptographically signed report from your encoded data.

**Basic pattern:**

```typescript
import { hexToBase64, type Runtime } from "@chainlink/cre-sdk"
import { encodeAbiParameters, parseAbiParameters } from "viem"

// Step 1: Encode your data using Viem
const encodedValue = encodeAbiParameters(parseAbiParameters("uint256 value"), [123456789n])

// Step 2: Generate the signed report
const report = runtime
  .report({
    encodedPayload: hexToBase64(encodedValue),
    encoderName: "evm",
    signingAlgo: "ecdsa",
    hashingAlgo: "keccak256",
  })
  .result()

// Step 3: Submit via HTTP (covered in next section)
```

The `runtime.report()` method works the same way whether you're encoding a single value or a struct—just use Viem's `encodeAbiParameters()` with the appropriate ABI types. For detailed examples on encoding single values, structs, and complex types, see the [Writing Data Onchain](/cre/guides/workflow/using-evm-client/onchain-write/writing-data-onchain) guide.

## Using `sendReport()` (recommended approach)

Use the high-level `httpClient.sendRequest()` pattern with `sendRequester.sendReport()`:

```typescript
import {
  HTTPClient,
  consensusIdenticalAggregation,
  ok,
  type Runtime,
  type HTTPSendRequester,
  type Report,
} from "@chainlink/cre-sdk"

interface SubmitResponse {
  success: boolean
}

const submitReportViaHTTP = (sendRequester: HTTPSendRequester, report: Report): SubmitResponse => {
  const response = sendRequester.sendReport(report, formatReportSimple).result()

  if (!ok(response)) {
    throw new Error(`API returned error: status=${response.statusCode}`)
  }

  runtime.log(`Report submitted successfully, status: ${response.statusCode}`)
  return { success: true }
}

// In your trigger callback
const onCronTrigger = (runtime: Runtime<Config>): MyResult => {
  const httpClient = new HTTPClient()

  // Assume 'report' was generated earlier in your workflow

  // Call the submission function
  const result = httpClient
    .sendRequest(
      runtime,
      (sendRequester: HTTPSendRequester) => submitReportViaHTTP(sendRequester, report),
      consensusIdenticalAggregation<SubmitResponse>()
    )()
    .result()

  return {}
}
```

## Complete working example

This example shows a workflow that:

1. Generates a report from a single value
2. Submits it to an HTTP API
3. Uses the simple "report in body" format

```typescript
import {
  CronCapability,
  HTTPClient,
  Runner,
  consensusIdenticalAggregation,
  hexToBase64,
  ok,
  type Runtime,
  type Report,
  type CronPayload,
  type HTTPSendRequester,
  type ReportResponse,
  type RequestJson,
} from "@chainlink/cre-sdk"
import { encodeAbiParameters, parseAbiParameters } from "viem"

interface Config {
  apiUrl: string
  schedule: string
}

interface SubmitResponse {
  success: boolean
}

type MyResult = Record<string, never>

const initWorkflow = (config: Config) => {
  const cron = new CronCapability()

  return [cron.handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
}

// Transformation function: defines how the API expects the report
const formatReportForMyAPI = (r: ReportResponse): RequestJson => {
  return {
    url: "https://webhook.site/your-unique-id", // Replace with your API
    method: "POST",
    body: Buffer.from(r.rawReport).toString("base64"),
    headers: {
      "Content-Type": "application/octet-stream",
      "X-Report-SeqNr": r.seqNr.toString(),
    },
    cacheSettings: {
      readFromCache: true, // Prevent duplicate submissions
      maxAgeMs: 60000, // Accept cached responses up to 60 seconds old
    },
  }
}

// Function that submits the report via HTTP
const submitReportViaHTTP = (sendRequester: HTTPSendRequester, report: Report): SubmitResponse => {
  runtime.log("Submitting report to API")

  const response = sendRequester.sendReport(report, formatReportForMyAPI).result()

  runtime.log(`Report submitted - status: ${response.statusCode}, bodyLength: ${response.body.length}`)

  if (!ok(response)) {
    const bodyText = new TextDecoder().decode(response.body)
    throw new Error(`API error: status=${response.statusCode}, body=${bodyText}`)
  }

  return { success: true }
}

const onCronTrigger = (runtime: Runtime<Config>, payload: CronPayload): MyResult => {
  // Step 1: Generate a report (example: a single uint256 value)
  const myValue = 123456789n
  runtime.log(`Generating report with value: ${myValue}`)

  // Encode the value using Viem
  const encodedValue = encodeAbiParameters(parseAbiParameters("uint256 value"), [myValue])

  // Generate the report
  const reportResponse = runtime
    .report({
      encodedPayload: hexToBase64(encodedValue),
      encoderName: "evm",
      signingAlgo: "ecdsa",
      hashingAlgo: "keccak256",
    })
    .result()

  runtime.log("Report generated successfully")

  // Step 2: Submit the report via HTTP
  const httpClient = new HTTPClient()

  const submitResult = httpClient
    .sendRequest(
      runtime,
      (sendRequester: HTTPSendRequester) => submitReportViaHTTP(sendRequester, reportResponse),
      consensusIdenticalAggregation<SubmitResponse>()
    )()
    .result()

  runtime.log(`Workflow completed successfully, submitted: ${submitResult.success}`)
  return {}
}

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

### Configuration file (`config.json`)

```json
{
  "apiUrl": "https://webhook.site/your-unique-id",
  "schedule": "0 * * * *"
}
```

### Testing with webhook.site

1. Go to [webhook.site](https://webhook.site/) and get a unique URL
2. Update `config.json` with your webhook URL
3. Run the simulation:
   ```bash
   cre workflow simulate my-workflow --target staging-settings
   ```
4. Check webhook.site to see the report data received

## Advanced: Low-level pattern

For complex scenarios where you need more control, use `clientCapability.sendReport()` with `runtime.runInNodeMode()`:

```typescript
import {
  HTTPClient,
  consensusIdenticalAggregation,
  ok,
  type Runtime,
  type NodeRuntime,
  type Report,
} from "@chainlink/cre-sdk"

const onCronTrigger = (runtime: Runtime<Config>): MyResult => {
  // Assume 'report' was generated earlier

  const result = runtime
    .runInNodeMode((nodeRuntime: NodeRuntime<Config>) => {
      const httpClient = new HTTPClient()

      const response = httpClient.sendReport(nodeRuntime, report, formatReportSimple).result()

      if (!ok(response)) {
        throw new Error(`API error: ${response.statusCode}`)
      }

      return { success: true }
    }, consensusIdenticalAggregation<SubmitResponse>())()
    .result()

  return {}
}
```

## Best practices

1. **Always use `cacheSettings`**: Include caching in every transformation function to prevent worst-case duplicate submission scenarios
2. **Implement API-side deduplication**: Your receiving API must implement deduplication using the **hash of the report** (`keccak256(rawReport)`) to detect and reject duplicate submissions
3. **Verify signatures before processing**: Your API must verify the cryptographic signatures against DON public keys before trusting report data (see note below about signature verification)
4. **Match your API's format exactly**: Study your API's documentation to understand the expected format (binary, JSON, headers, etc.)
5. **Handle errors gracefully**: Check HTTP status codes and provide meaningful error messages

> **CAUTION: Signature verification is your responsibility**
>
> Unlike onchain submissions (where the `KeystoneForwarder` contract verifies signatures), **HTTP submissions require your API to verify signatures** before trusting the report data.

**Documentation coming soon**: "Verifying CRE Reports Offchain" guide.

## Troubleshooting

**"failed to send report" error**

- Verify your API URL is correct and accessible
- Check that your transformation function returns a valid `RequestJson`
- Ensure your API can handle binary data if you're sending raw bytes (base64-encoded)

**API returns 400/422 errors**

- Your report format likely doesn't match what your API expects
- Check if your API expects base64 encoding, JSON wrapping, or specific headers

## Learn more

- **[HTTP Client SDK Reference](/cre/reference/sdk/http-client-ts)** — Complete API reference including `sendReport()` and `ReportResponse`
- **[POST Requests](/cre/guides/workflow/using-http-client/post-request)** — Learn about HTTP request patterns and caching
- **[Writing Data Onchain](/cre/guides/workflow/using-evm-client/onchain-write/writing-data-onchain)** — Detailed guide on encoding single values, structs, and complex types using Viem
- **[Submitting Reports Onchain](/cre/guides/workflow/using-evm-client/onchain-write/submitting-reports-onchain)** — Alternative: Submit reports to smart contracts instead of HTTP