# Verifying CRE Reports Offchain
Source: https://docs.chain.link/cre/guides/workflow/using-http-client/verifying-reports-offchain-go
Last Updated: 2026-05-20

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

This guide is for the **receiver** side: you already received a CRE report package — typically via HTTP from a [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go) sender workflow — and need to **prove it is authentic** before using the payload.

When a workflow delivers results via HTTP (or another offchain channel), nothing onchain automatically validates the report. **You must verify signatures before trusting the data.**

There are two ways to verify, depending on who receives the POST:

| My receiver is...                               | Use                                                                                                                    |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **My own API or server** (Go HTTP server, etc.) | [Verifying outside CRE](#verifying-outside-cre) — standard crypto libraries, no CRE SDK needed                         |
| **A CRE workflow** with an HTTP trigger         | [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) — see [CRE receiver workflow](#cre-receiver-workflow) |

Both paths run the same cryptographic checks against the same onchain Capability Registry. The difference is whether you use the CRE SDK's helper (which requires `runtime`) or implement it yourself with standard libraries.

## What is a CRE report?

A **[CRE report](/cre/key-terms#report-cre-report)** is a DON-signed package another workflow (or system) created with [`runtime.GenerateReport()`](/cre/guides/workflow/using-evm-client/onchain-write/generating-reports-single-values). You receive its bytes over HTTP (or another channel) as `rawReport`, `reportContext`, and `signatures`. Before you use the encoded payload, you must confirm the signatures match authorized DON signers on the Capability Registry.

See [Key Terms: Report](/cre/key-terms#report-cre-report) for how reports are created and delivered. Pair this guide with [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go) on the sender side.

**Receiver flow (both paths):**

1. Your endpoint (API or CRE HTTP trigger) receives the POST payload.
2. Decode hex fields into bytes.
3. Verify signatures against the Capability Registry.
4. Use the trusted payload body in your logic.

## What you'll learn

- When to verify reports offchain vs relying on onchain forwarders
- How [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) validates signatures and reads metadata (inside a CRE workflow)
- How to build a CRE receiver workflow that accepts reports over HTTP
- How to verify reports in your own API server without a CRE receiver workflow
- How to restrict verification to specific CRE environments or zones

## Prerequisites

- **A report payload to verify** — three hex fields: `report`, `context`, `signatures`. If you don't have one yet, follow [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go) to create a sender workflow and capture its output first.
- **SDK**: `cre-sdk-go` v1.8.0 or later (for the CRE receiver workflow path)
- For HTTP-triggered receivers: [HTTP Trigger configuration](/cre/guides/workflow/using-triggers/http-trigger/configuration-go)

## Onchain vs offchain verification

| Aspect               | Offchain ([`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport)) | Onchain (`KeystoneForwarder`)     |
| -------------------- | --------------------------------------------------------------------------- | --------------------------------- |
| **Where it runs**    | Inside your CRE workflow callback                                           | In a smart contract transaction   |
| **Signature check**  | Local `ecrecover` on report hash                                            | Contract logic onchain            |
| **Signer allowlist** | Read from Capability Registry (`getDON`, `getNodesByP2PIds`)                | Forwarder + registry              |
| **Typical use**      | CRE receiver workflows with an HTTP trigger                                 | Consumer contracts via `onReport` |

Offchain verification still uses **onchain data as a trust anchor**: the first time a DON is seen, the SDK reads the production Capability Registry on Ethereum Mainnet to learn `f` and authorized signer addresses.

Default (`cre.ProductionEnvironment()`):

- **Chain**: Ethereum Mainnet (chain selector `5009297550715157269`)
- **Registry**: `0x76c9cf548b4179F8901cda1f8623568b58215E62`

## How verification works

At its core, verification answers one question: did enough authorized nodes from the right DON sign this exact report? To answer it, the receiver decodes the report's metadata header to find which DON produced it, asks the onchain Capability Registry for that DON's authorized signers and quorum threshold (`f`), then checks that at least `f+1` of the provided signatures come from those addresses. Both paths in this guide — the outside-CRE program and the CRE receiver workflow — run this same sequence:

1. **Parse the report header** from `rawReport` (109-byte metadata + body).
2. **Fetch DON info** from the registry (if not cached): fault tolerance `f` and signer addresses.
3. **Verify signatures**: compute `keccak256(keccak256(rawReport) || reportContext)`, recover signers, require **f+1** valid signatures from authorized nodes.
4. **Return a `*cre.Report`** with accessors for workflow ID, owner, execution ID, body, and more.

If verification fails, [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) returns an error (for example, `ErrUnknownSigner`, `ErrWrongSignatureCount`, or registry read failure).

## Verifying outside CRE

[`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) requires the CRE `runtime` object and can only run inside a CRE workflow callback. If your API server receives reports directly, you can verify without the CRE SDK using standard libraries.

If you followed the [submit guide](/cre/guides/workflow/using-http-client/submitting-reports-http-go), you already have a JSON payload with `report`, `context`, and `signatures` as hex strings — this section shows how to verify it.

1. Create a new folder for your verification program and initialize a module:

   ```bash
   mkdir verify-report && cd verify-report
   go mod init verify-report
   go get github.com/ethereum/go-ethereum
   ```

2. Save the following as `main.go`.

   Parses the 109-byte report header to identify the DON, reads the authorized signer list and fault tolerance `f` from the Capability Registry on Ethereum Mainnet, then recovers each signature and requires at least `f+1` to match. The ABI layout mirrors [`report_verification.go` in cre-sdk-go](https://github.com/smartcontractkit/cre-sdk-go).

   ```go
   // verify-report/main.go
   package main

   import (
   	"context"
   	"encoding/binary"
   	"encoding/hex"
   	"fmt"
   	"math/big"
   	"sync"

   	"github.com/ethereum/go-ethereum"
   	"github.com/ethereum/go-ethereum/common"
   	"github.com/ethereum/go-ethereum/crypto"
   	"github.com/ethereum/go-ethereum/ethclient"
   )

   const (
   	reportHeaderLength = 109
   	capabilityRegistry = "0x76c9cf548b4179F8901cda1f8623568b58215E62"
   )

   // parseHeader extracts key fields from the 109-byte rawReport metadata header.
   func parseHeader(rawReport []byte) (donId uint32, workflowId string, body []byte, err error) {
   	if len(rawReport) < reportHeaderLength {
   		return 0, "", nil, fmt.Errorf("rawReport too short: need %d bytes, got %d", reportHeaderLength, len(rawReport))
   	}
   	donId = binary.BigEndian.Uint32(rawReport[37:41])
   	workflowId = hex.EncodeToString(rawReport[45:77])
   	body = rawReport[reportHeaderLength:]
   	return
   }

   // reportHash computes keccak256(keccak256(rawReport) || reportContext).
   func reportHash(rawReport, reportContext []byte) []byte {
   	inner := crypto.Keccak256(rawReport)
   	return crypto.Keccak256(append(inner, reportContext...))
   }

   // padUint256 encodes a uint64 as a big-endian 32-byte ABI word.
   func padUint256(v uint64) []byte {
   	b := make([]byte, 32)
   	binary.BigEndian.PutUint64(b[24:], v)
   	return b
   }

   type donInfo struct {
   	f       int
   	signers map[common.Address]bool
   }

   // signerCache caches registry lookups by DON ID. Registry data only changes
   // during DON reconfiguration; clear and retry if you see unexpected signer errors after a DON upgrade.
   var signerCache sync.Map

   // fetchSigners calls the Capability Registry on Ethereum Mainnet to get
   // the fault tolerance f and authorized signer addresses for the given DON.
   // Makes two eth_call reads: getDON(donId) then getNodesByP2PIds(nodeP2PIds).
   // Result is cached by DON ID.
   func fetchSigners(client *ethclient.Client, donId uint32) (f int, signers map[common.Address]bool, err error) {
   	if cached, ok := signerCache.Load(donId); ok {
   		info := cached.(donInfo)
   		return info.f, info.signers, nil
   	}

   	registry := common.HexToAddress(capabilityRegistry)
   	ctx := context.Background()

   	// Step 1: getDON(uint32 donId) — selector keccak256("getDON(uint32)")[0:4] = 0x23537405
   	var donIdPadded [32]byte
   	binary.BigEndian.PutUint32(donIdPadded[28:], donId)
   	getDONCalldata := append([]byte{0x23, 0x53, 0x74, 0x05}, donIdPadded[:]...)

   	getDONBytes, err := client.CallContract(ctx, ethereum.CallMsg{To: &registry, Data: getDONCalldata}, nil)
   	if err != nil {
   		return 0, nil, fmt.Errorf("getDON call failed: %w", err)
   	}
   	if len(getDONBytes) < 224 {
   		return 0, nil, fmt.Errorf("getDON response too short: %d bytes", len(getDONBytes))
   	}

   	// Response layout (see report_verification.go in cre-sdk-go for full ABI documentation):
   	//   slot 3 (bytes  96-127): f               (uint8, zero-padded to 32)
   	//   slot 6 (bytes 192-223): ptr[nodeP2PIds] relative to tupleStart = 32
   	f = int(new(big.Int).SetBytes(getDONBytes[96:128]).Int64())
   	nodeP2PIdsPtr := int(new(big.Int).SetBytes(getDONBytes[192:224]).Int64())
   	nodeCountOff := 32 + nodeP2PIdsPtr
   	nodeCount := int(new(big.Int).SetBytes(getDONBytes[nodeCountOff : nodeCountOff+32]).Int64())

   	nodeP2PIds := make([][]byte, nodeCount)
   	for i := 0; i < nodeCount; i++ {
   		start := nodeCountOff + 32 + i*32
   		id := make([]byte, 32)
   		copy(id, getDONBytes[start:start+32])
   		nodeP2PIds[i] = id
   	}

   	if nodeCount == 0 {
   		info := donInfo{f: f, signers: nil}
   		signerCache.Store(donId, info)
   		return f, nil, nil
   	}

   	// Step 2: getNodesByP2PIds(bytes32[]) — selector 0x05a51966
   	// ABI-encode bytes32[]: [ptr=32][count][id0]...[idN]
   	getNodesCalldata := append([]byte{0x05, 0xa5, 0x19, 0x66}, padUint256(32)...)
   	getNodesCalldata = append(getNodesCalldata, padUint256(uint64(nodeCount))...)
   	for _, id := range nodeP2PIds {
   		getNodesCalldata = append(getNodesCalldata, id...)
   	}

   	getNodesBytes, err := client.CallContract(ctx, ethereum.CallMsg{To: &registry, Data: getNodesCalldata}, nil)
   	if err != nil {
   		return 0, nil, fmt.Errorf("getNodesByP2PIds call failed: %w", err)
   	}
   	if len(getNodesBytes) < 64 {
   		return 0, nil, fmt.Errorf("getNodesByP2PIds response too short: %d bytes", len(getNodesBytes))
   	}

   	// Response layout: [outerPtr][count][elem0-ptr][elem1-ptr]...[tuple0][tuple1]...
   	// Each NodeInfo tuple (9 slots × 32 bytes): slot 3 = signer bytes32, first 20 bytes = address
   	outerPtr := int(new(big.Int).SetBytes(getNodesBytes[0:32]).Int64())
   	returnedCount := int(new(big.Int).SetBytes(getNodesBytes[outerPtr : outerPtr+32]).Int64())
   	const nodeTupleHead = 288 // 9 slots × 32 bytes

   	signers = make(map[common.Address]bool, returnedCount)
   	for i := 0; i < returnedCount; i++ {
   		elemPtrOff := outerPtr + 32 + i*32
   		elemPtr := int(new(big.Int).SetBytes(getNodesBytes[elemPtrOff : elemPtrOff+32]).Int64())
   		tupleBase := outerPtr + 32 + elemPtr
   		if tupleBase+nodeTupleHead > len(getNodesBytes) {
   			break
   		}
   		signerSlot := tupleBase + 3*32
   		addr := common.BytesToAddress(getNodesBytes[signerSlot : signerSlot+20])
   		signers[addr] = true
   	}

   	signerCache.Store(donId, donInfo{f: f, signers: signers})
   	return f, signers, nil
   }

   // verifyReport checks that ≥ f+1 signatures are from authorized DON signers.
   func verifyReport(rawReport []byte, signatures [][]byte, reportContext []byte, client *ethclient.Client) error {
   	donId, _, _, err := parseHeader(rawReport)
   	if err != nil {
   		return err
   	}
   	f, signers, err := fetchSigners(client, donId)
   	if err != nil {
   		return err
   	}
   	hash := reportHash(rawReport, reportContext)
   	required := f + 1
   	valid := 0

   	for _, sig := range signatures {
   		if len(sig) != 65 {
   			continue
   		}
   		norm := make([]byte, 65)
   		copy(norm, sig)
   		if norm[64] >= 27 {
   			norm[64] -= 27 // normalize recovery byte
   		}
   		pubKey, err := crypto.SigToPub(hash, norm)
   		if err != nil {
   			continue
   		}
   		addr := crypto.PubkeyToAddress(*pubKey)
   		if signers[addr] {
   			valid++
   		}
   		if valid >= required {
   			return nil
   		}
   	}
   	return fmt.Errorf("insufficient valid signatures: %d/%d", valid, required)
   }

   // Usage — create the client once for your server, reuse it across requests:
   //
   //   client, err := ethclient.Dial(os.Getenv("ETH_MAINNET_RPC_URL"))
   //
   // For each incoming report POST:
   //   if err := verifyReport(rawReport, signatures, reportContext, client); err != nil {
   //       // reject the request
   //   }
   //   // verified: use parseHeader(rawReport) to read the payload body
   ```

3. Set your RPC URL and run:

   ```bash
   export ETH_MAINNET_RPC_URL=https://your-mainnet-rpc-endpoint
   go run main.go
   ```

4. Check the output.

   With a **sim-signed report** (from the submit guide sender simulation):

   ```
   insufficient valid signatures: 0/4
   ```

   This is correct. Simulation uses local test keys that are not registered in the mainnet Capability Registry. The registry call succeeded and returned the real signer list; none of the sim signatures matched it. The verification logic is working as intended.

   With a **production-signed report** from a deployed sender, `verifyReport` returns `nil`. Read the payload:

   ```go
   donId, workflowId, body, err := parseHeader(rawReport)
   // body is the ABI-encoded payload the sender embedded
   ```

> **CAUTION: Cache registry results**
>
> `fetchSigners` caches by DON ID using a `sync.Map` for the lifetime of the process. Registry data only changes during DON reconfiguration. If you see unexpected `ErrUnknownSigner` errors after a DON upgrade, clear the cache (for example, by restarting the process) and retry.

## CRE receiver workflow

Use this path if you want the receiver itself to be a CRE workflow with an HTTP trigger. [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) handles Capability Registry reads and caching automatically.

> **CAUTION: Educational Example Disclaimer**
>
> This page includes an educational example to use a Chainlink system, product, or service and is provided to
> demonstrate how to interact with Chainlink's systems, products, and services to integrate them into your own. This
> template is provided "AS IS" and "AS AVAILABLE" without warranties of any kind, it has not been audited, and it may be
> missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the
> code in this example in a production environment without completing your own audits and application of best practices.
> Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs
> that are generated due to errors in code.

> **NOTE: Using the private registry?**
>
> This guide uses the **Capability Registry** (DON signers), not the **workflow registry** where you deploy. [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) works the same way regardless of which registry you deployed with. For HTTP-triggered receivers, use the [enterprise gateway URL](/cre/guides/operations/deploying-to-private-registry-go#http-triggers-with-the-private-registry). Local simulation may need an `ethereum-mainnet` RPC in `project.yaml` for registry reads.

### Testing with simulation

If you ran the [submit guide complete example](/cre/guides/workflow/using-http-client/submitting-reports-http-go#complete-working-example), you already copied JSON from webhook.site. Use that payload here.

> **NOTE: CRE project required**
>
> The directory you run `cre` from must contain a `project.yaml`. If you do not have one, run `cre init` from your project folder or follow the [Getting Started guide](/cre/getting-started/overview).

1. Save the webhook JSON as `test-report-payload.json` inside your receiver workflow folder:

   ```
   verify-report-receiver/test-report-payload.json
   ```

2. Create a `verify-report-receiver/` folder in your CRE project with the following files.

   `config.staging.json` — enables wiring tests without mainnet signer validation:

   ```json
   {
     "skipSignatureVerification": true
   }
   ```

   `main.go` — uses an empty HTTP trigger (add `AuthorizedKeys` before deploying):

   ```go
   // verify-report-receiver/main.go
   //go:build wasip1

   package main

   import (
   	"encoding/hex"
   	"encoding/json"
   	"log/slog"

   	"github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
   	"github.com/smartcontractkit/cre-sdk-go/cre"
   	"github.com/smartcontractkit/cre-sdk-go/cre/wasm"
   )

   type Config struct {
   	SkipSignatureVerification bool `json:"skipSignatureVerification"`
   }

   type parsedPayload struct {
   	Report     string   `json:"report"`
   	Context    string   `json:"context"`
   	Signatures []string `json:"signatures"`
   }

   func InitWorkflow(_ *Config, _ *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[*Config], error) {
   	return cre.Workflow[*Config]{
   		cre.Handler(http.Trigger(&http.Config{}), run),
   	}, nil
   }

   func run(cfg *Config, runtime cre.Runtime, payload *http.Payload) (bool, error) {
   	var parsed parsedPayload
   	if err := json.Unmarshal(payload.Input, &parsed); err != nil {
   		return false, err
   	}

   	rawReport, err := hex.DecodeString(parsed.Report)
   	if err != nil {
   		return false, err
   	}
   	reportContext, err := hex.DecodeString(parsed.Context)
   	if err != nil {
   		return false, err
   	}
   	sigs := make([][]byte, len(parsed.Signatures))
   	for i, sigHex := range parsed.Signatures {
   		sigs[i], err = hex.DecodeString(sigHex)
   		if err != nil {
   			return false, err
   		}
   	}

   	report, err := cre.ParseReportWithConfig(runtime, rawReport, sigs, reportContext, cre.ReportParseConfig{
   		SkipSignatureVerification: cfg.SkipSignatureVerification,
   	})
   	if err != nil {
   		return false, err
   	}

   	runtime.Logger().Info("Verified report",
   		"workflowId", report.WorkflowID(),
   		"executionId", report.ExecutionID(),
   		"donId", report.DONID(),
   	)

   	_ = report.Body()
   	return true, nil
   }

   func main() {
   	wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
   }
   ```

3. From the CRE project root, run the simulation:

   ```bash
   cre workflow simulate verify-report-receiver \
     --target staging-settings \
     --non-interactive \
     --trigger-index 0 \
     --http-payload verify-report-receiver/test-report-payload.json
   ```

4. Check the output.

   A successful wiring run logs the decoded report metadata and returns successfully:

   ```
   [USER LOG] msg="Verified report" workflowId=... executionId=... donId=1
   ✓ Workflow Simulation Result: true
   ```

   This confirms JSON decoding, hex parsing, and [`cre.ParseReportWithConfig`](/cre/reference/sdk/core-go#creparsereportwithconfig) are wired correctly. Signatures are not verified against the mainnet registry in this mode — that requires a production-signed report from a deployed sender.

### Deploying to production

The `run` handler you simulated is the same code you deploy. For production, make two configuration changes:

1. **Remove `skipSignatureVerification`** from your target config (or omit it; the default is `false`). Reports from a deployed sender must pass real signature verification.
2. **Add `AuthorizedKeys`** to your HTTP trigger — required for deployed workflows, not just simulation. See [HTTP Trigger configuration](/cre/guides/workflow/using-triggers/http-trigger/configuration-go).

> **CAUTION: Hex encoding**
>
> The example expects **hex strings without a `0x` prefix** in JSON. Adjust decoding if your API sends `0x`-prefixed values or base64 instead. Pattern 4 in the [submit guide](/cre/guides/workflow/using-http-client/submitting-reports-http-go#pattern-4-json-formatted-report) uses base64 by default; use [Pattern 4 for offchain verification (hex)](/cre/guides/workflow/using-http-client/submitting-reports-http-go#pattern-4-for-offchain-verification-hex) when testing this receiver.

## Report payload format

When a sender POSTs a [CRE report](/cre/key-terms#report-cre-report) as JSON for offchain verification, receivers need three fields. The JSON key is `context` even though the SDK field is `ReportContext`. See [Payload contract](/cre/guides/workflow/using-http-client/submitting-reports-http-go#payload-contract-if-you-verify-offchain) in the submit guide for how the sender produces these fields.

| JSON field   | SDK field       | Description                                                     |
| ------------ | --------------- | --------------------------------------------------------------- |
| `report`     | `RawReport`     | Hex-encoded bytes (metadata header + workflow payload), no `0x` |
| `context`    | `ReportContext` | Hex-encoded config digest + sequence number                     |
| `signatures` | `Sigs`          | Array of hex-encoded 65-byte ECDSA signatures, no `0x`          |

## API reference

For full signatures, types, and `ReportParseConfig` options (including deferred verification), see [SDK Reference: Core: Report verification](/cre/reference/sdk/core-go#report-verification).

### `*cre.Report` accessors

After a successful [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport):

| Method            | Description                               |
| ----------------- | ----------------------------------------- |
| `WorkflowID()`    | Workflow hash (`bytes32` as hex)          |
| `WorkflowOwner()` | Deployer address (hex)                    |
| `WorkflowName()`  | Workflow name field from metadata         |
| `ExecutionID()`   | Unique execution identifier               |
| `DONID()`         | DON that produced the report              |
| `Timestamp()`     | Report timestamp (Unix seconds)           |
| `Body()`          | Encoded payload after the 109-byte header |
| `SeqNr()`         | Sequence number from report context       |
| `ConfigDigest()`  | Config digest from report context         |

## Best practices

1. **Verify before side effects**: Call [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) before writing to databases, chains, or external systems.
2. **Permission on metadata**: After verification, check `WorkflowID()`, `WorkflowOwner()`, or `DONID()` match your expectations.
3. **Deduplicate by execution ID**: Use `ExecutionID()` or `keccak256(rawReport)` to reject replays (see [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go#understanding-cachesettings-for-reports)).
4. **Do not skip signature verification in production** unless you have another trust path.

## Troubleshooting

**`ErrUnknownSigner` / `invalid signature` in sim with fresh webhook JSON**

- **Expected** when using default [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport) on a **sim-signed** report: simulator DON keys do not match mainnet registry signers.
- For local wiring tests, use `SkipSignatureVerification: true`. For real crypto verify, use a **deployed sender** or production-signed reports.

**`ErrUnknownSigner` (deployed)**

- Signatures may be from a different DON or stale registry config.
- Confirm the sender workflow used production CRE and the report was not tampered with.

**Wrong `--http-payload` path**

- Invoke `cre` from the **project root**. Use `verify-report-receiver/test-report-payload.json`, not a bare filename unless your cwd matches.

**Receiver JSON / hex decode error**

- You copied a **binary** webhook body instead of Pattern 4 JSON with hex fields.

**`ErrWrongSignatureCount`**

- At least **f+1** valid signatures are required.

**`could not read from chain ...`** *(CRE receiver workflow only)*

- Registry read failed (RPC/network). Configure an **`ethereum-mainnet` RPC** in `project.yaml` — required for default [`cre.ParseReport()`](/cre/reference/sdk/core-go#creparsereport), including during sim.

**`ErrRawReportTooShort`**

- `rawReport` is missing the 109-byte metadata header.

## Learn more

- **[Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go):** sender workflow; create and POST the report
- **[SDK Reference: Core: Report verification](/cre/reference/sdk/core-go#report-verification):** `ParseReport`, `Report`, and `ReportParseConfig`
- **[HTTP Trigger Overview](/cre/guides/workflow/using-triggers/http-trigger/overview-go):** trigger deployed receiver workflows
- **[Submitting Reports Onchain](/cre/guides/workflow/using-evm-client/onchain-write/submitting-reports-onchain):** onchain forwarder verification path
- **[Building Consumer Contracts](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts):** permissioning `onReport` with workflow metadata