# 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 (usually via HTTP) 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.**

The CRE SDK provides `cre.ParseReport()` to do this inside a workflow. Verification runs offchain in your callback: signatures are checked with local cryptography, while authorized signer addresses are loaded via **read-only calls to the onchain Capability Registry** (default: Ethereum Mainnet). Results are cached per DON.

> **NOTE: Not your workflow deployment registry**
>
> This guide uses the **Capability Registry** (DON signers), not the **workflow registry** where you deploy (`private` or `onchain:ethereum-mainnet`). If you deployed with the [private registry](/cre/guides/operations/deploying-to-private-registry-go), `ParseReport` still works the same way. For an HTTP-triggered receiver, use the [enterprise gateway URL](/cre/guides/operations/deploying-to-private-registry-go#http-triggers-with-the-private-registry) when triggering deployed workflows. Local simulation may still need an `ethereum-mainnet` RPC in `project.yaml` for those registry reads, even though private deploy does not.

> **NOTE: Onchain verification is different**
>
> When you submit reports onchain through the `KeystoneForwarder`, the forwarder contract verifies signatures before calling your consumer's `onReport`. This guide covers **offchain** verification for HTTP and custom ingest paths. See [Submitting Reports Onchain](/cre/guides/workflow/using-evm-client/onchain-write/submitting-reports-onchain).

## Where this guide fits

| Question                     | Answer                                                                                                                                                                                              |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| What is the report?          | Same CRE report the **sender** created with `runtime.GenerateReport()`. See [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go#where-this-guide-fits). |
| Where does it come from?     | Another workflow (or system) already ran sender steps: logic → `GenerateReport()` → HTTP POST. You receive `rawReport`, `context`, and `signatures` in the request body.                            |
| What does this guide cover?  | Step 3 below: `cre.ParseReport()` before you use `Body()` or take side effects.                                                                                                                     |
| Same workflow as the sender? | Often **no:** common pattern is Workflow A (publish) and Workflow B (ingest with HTTP trigger).                                                                                                     |

**Receiver flow:**

1. HTTP trigger (or your API) receives the POST payload.
2. Decode hex fields into bytes.
3. `cre.ParseReport()`: verify signatures and read metadata.
4. Use trusted `Body()` in your logic.

Pair this guide with [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go) on the sender side. This guide covers local simulation first, then the deploy example with `AuthorizedKeys`.

## What you'll learn

- When to verify reports offchain vs relying on onchain forwarders
- How `cre.ParseReport()` validates signatures and reads metadata
- How to build a receiver workflow that accepts reports over HTTP
- How to restrict verification to specific CRE environments or zones

## Prerequisites

- **SDK**: `cre-sdk-go` v1.8.0 or later (report verification support)
- Familiarity with [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-go) (report structure and JSON payload patterns)
- For HTTP-triggered receivers: [HTTP Trigger configuration](/cre/guides/workflow/using-triggers/http-trigger/configuration-go)

## Onchain vs offchain verification

| Aspect               | Offchain (`cre.ParseReport`)                                 | 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**      | HTTP APIs, webhooks, ingest workflows                        | 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

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()` returns an error (for example, `ErrUnknownSigner`, `ErrWrongSignatureCount`, or registry read failure).

## Testing locally with simulation

After you run the [submit guide complete example](/cre/guides/workflow/using-http-client/submitting-reports-http-go#complete-working-example) and copy JSON from webhook.site, use this section to exercise a receiver workflow in simulation.

1. Save the webhook JSON as `test-report-payload.json` in your receiver workflow folder.
2. Register `http.Trigger(&http.Config{})` (empty config) for simulation.
3. From the **CRE project root**, run `cre workflow simulate` with `--http-payload verify-report-receiver/test-report-payload.json` (path relative to where you invoke `cre`).

### Minimal receiver for simulation

Use an empty HTTP trigger for sim. Set `SkipSignatureVerification: true` in staging config (or pass it to `ParseReportWithConfig`). The CLI delivers `--http-payload` file contents as `payload.Input` bytes.

`config.staging.json`:

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

```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)
}
```

### Sim wiring vs full verify

| Mode                   | Config                                                       | What it validates                                                                             |
| ---------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| **Wiring / decode**    | `SkipSignatureVerification: true` in `ParseReportWithConfig` | JSON + hex decode, metadata accessors, `Body()`                                               |
| **Full crypto verify** | Default `cre.ParseReport()` (production registry)            | Reports from a **deployed/production DON**. Sim-signed reports **fail** default verification. |

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

**Pass criteria**

- **Sim wiring:** `SkipSignatureVerification: true` in `ParseReportWithConfig`: logs show metadata and a successful handler return.
- **Full crypto verify:** default `ParseReport` with a **production-signed** report (not typical for sender-sim → receiver-sim alone).

`project.yaml` needs **`ethereum-mainnet` RPC** for default verify (registry reads).

## Complete example: HTTP receiver workflow (deploy)

Once simulation is working, update the trigger config for deployment with `AuthorizedKeys`.

It accepts JSON with hex `report`, `context`, and `signatures` from the submit guide’s [complete working example](/cre/guides/workflow/using-http-client/submitting-reports-http-go#complete-working-example) or [Pattern 4 for offchain verification (hex)](/cre/guides/workflow/using-http-client/submitting-reports-http-go#pattern-4-for-offchain-verification-hex), not base64 [Pattern 4](/cre/guides/workflow/using-http-client/submitting-reports-http-go#pattern-4-json-formatted-report) unless you change decoding.

```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 {
	AuthorizedKey string `json:"authorized_key"`
}

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

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

func (p *ParsedPayload) Decode() (*DecodedReport, error) {
	report := &DecodedReport{}
	var err error

	if report.Report, err = hex.DecodeString(p.Report); err != nil {
		return nil, err
	}
	if report.Context, err = hex.DecodeString(p.Context); err != nil {
		return nil, err
	}

	report.Sigs = make([][]byte, len(p.Sigs))
	for i, sigHex := range p.Sigs {
		report.Sigs[i], err = hex.DecodeString(sigHex)
		if err != nil {
			return nil, err
		}
	}

	return report, nil
}

type DecodedReport struct {
	Report  []byte
	Context []byte
	Sigs    [][]byte
}

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

	decoded, err := parsed.Decode()
	if err != nil {
		return false, err
	}

	report, err := cre.ParseReport(runtime, decoded.Report, decoded.Sigs, decoded.Context)
	if err != nil {
		return false, err
	}

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

	// Use report.Body() for your application logic (ABI-encoded payload from the sender workflow)
	_ = report.Body()

	return true, nil
}

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

**What's happening:**

1. An external system POSTs hex-encoded `report`, `context`, and `signatures` to your HTTP trigger.
2. `cre.ParseReport()` verifies signatures against the production CRE registry.
3. On success, you read metadata and `Body()` safely.

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

Receivers need three JSON fields. The JSON key is `context` even though the SDK field is `ReportContext`:

| 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`          |

The `reportContext` layout used by the SDK:

- Bytes 0–31: config digest
- Bytes 32–39: sequence number (big-endian `uint64`)

## API reference

See [SDK Reference: Core: Report verification](/cre/reference/sdk/core-go#report-verification) for full signatures, types, and errors.

### `cre.ParseReport()`

```go
func ParseReport(runtime Runtime, rawReport []byte, signatures [][]byte, reportContext []byte) (*Report, error)
```

Parses and verifies a report against the production CRE environment. Use `ParseReportWithConfig` for custom environments or zones.

### `*cre.Report` accessors

After a successful parse:

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

### `cre.ReportParseConfig`

```go
config := cre.ReportParseConfig{
    AcceptedZones: []cre.Zone{
        cre.ZoneFromEnvironment(cre.ProductionEnvironment(), 1),
    },
    AcceptedEnvironments: []cre.Environment{cre.ProductionEnvironment()},
    SkipSignatureVerification: false,
}
report, err := cre.ParseReportWithConfig(runtime, rawReport, sigs, reportContext, config)
```

| Field                       | Description                                                                                                |
| --------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `AcceptedEnvironments`      | Registry environments to check (defaults to production)                                                    |
| `AcceptedZones`             | Restrict to specific DON IDs within an environment                                                         |
| `SkipSignatureVerification` | Parse header only; call `report.VerifySignatures()` or `VerifySignaturesWithConfig()` afterward when ready |

### Deferred verification

This is a **different pattern** from the simulation testing use of `SkipSignatureVerification`. In testing, you skip verification permanently. Here, you parse the header first to inspect metadata (such as `WorkflowID()` or `DONID()` for filtering), then call `VerifySignatures` in a separate step — useful when you want to gate registry reads on workflow identity checks.

If you set `SkipSignatureVerification: true` in `ParseReportWithConfig`, parse the header first, then verify:

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

// Optional: inspect report.WorkflowID(), report.DONID(), etc. before registry reads

if err := report.VerifySignatures(runtime); err != nil {
    return false, err
}
```

## Best practices

1. **Verify before side effects**: Call `cre.ParseReport()` 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 `ParseReport` 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 ...`**

- Registry read failed (RPC/network). Configure **`ethereum-mainnet` RPC** in `project.yaml` (required for default verify, including 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