> ## Documentation Index
> Fetch the complete documentation index at: https://docs.precipiq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How the Precipiq hash chain works

> Every decision record stores a SHA-256 hash of its predecessor, creating a tamper-evident chain that lets you verify the entire ledger with a single API call.

Every decision record in Precipiq stores the SHA-256 hash of its predecessor in its `prev_hash` field. The record's own `hash` is then computed over a canonical serialisation that includes `prev_hash`. This means tampering with any historical record would break the chain at every subsequent record — detectable in O(n) by a single `verify_chain` call. The chain is append-only and per-org: each organisation has its own sequence, cross-tenant interference is structurally impossible, and verifying one org's chain never touches another's data.

The first record an organisation writes uses `prev_hash = "0" * 64` as a sentinel genesis. From there, every new record extends the chain by exactly one link.

## How hashing works

Hashing runs on a canonical JSON encoding of eight specific fields:

```
action_type, agent_id, confidence, inputs, org_id,
outputs, prev_hash, timestamp
```

These eight fields are serialised with `sort_keys=True, separators=(',', ':'), ensure_ascii=False`, UTF-8 encoded, and SHA-256 digested. The hex digest is the record's `hash`. Because the recipe is deterministic and language-independent, a Python SDK caller and a TypeScript caller producing the same logical record compute bit-identical hashes.

### Fields not included in the hash

The following fields are stored on the record but are **not** part of its hash identity:

* `id` — assigned server-side at insert; not under the caller's control.
* `hash`, `created_at` — computed or stamped during persistence; not inputs to the hash.
* `alternatives`, `human_in_loop`, `meta` — caller-supplied metadata free to evolve in future versions without invalidating the chain.

<Warning>
  If you are verifying a chain externally, re-hash using only the eight identity fields above. Re-hashing with the wider record shape produces a different digest and makes the chain appear broken when it is not.
</Warning>

## Verifying the chain

The SDK does not yet wrap the verify endpoint. Call it directly via REST.

<CodeGroup>
  ```bash REST theme={null}
  curl -s https://api.precipiq.dev/api/v1/decisions/chain/verify \
    -H 'X-Precipiq-Key: pq_test_demo_key_REPLACE_ME'
  ```

  ```python Python theme={null}
  import httpx

  r = httpx.get(
      "https://api.precipiq.dev/api/v1/decisions/chain/verify",
      headers={"X-Precipiq-Key": "pq_test_demo_key_REPLACE_ME"},
      timeout=10.0,
  )
  r.raise_for_status()
  report = r.json()["data"]
  if not report["is_valid"]:
      raise RuntimeError(
          f"chain broken at {report['first_broken_link']}; "
          f"{report['records_checked']} records inspected"
      )
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch(
    'https://api.precipiq.dev/api/v1/decisions/chain/verify',
    { headers: { 'X-Precipiq-Key': 'pq_test_demo_key_REPLACE_ME' } },
  );
  if (!res.ok) throw new Error(`verify failed: ${res.status}`);
  const report = (await res.json()).data;
  if (!report.is_valid) {
    throw new Error(
      `chain broken at ${report.first_broken_link}; ` +
      `${report.records_checked} records inspected`,
    );
  }
  ```
</CodeGroup>

The endpoint walks every record in chronological order, recomputes each hash, and stops at the first mismatch. An `is_valid: true` response is constant-time small regardless of how many records passed — verification is cheap enough to run on a cron.

### What the response contains

| Field               | Type    | Description                                                        |
| ------------------- | ------- | ------------------------------------------------------------------ |
| `is_valid`          | boolean | `true` if the entire chain is intact.                              |
| `records_checked`   | integer | Number of records walked before stopping.                          |
| `first_broken_link` | string  | The `id` of the first record where the hash did not match, if any. |

<Tip>
  Run chain verification on a nightly cron and alert if `is_valid` is ever `false`. A broken chain is a high-severity signal — it means a record was altered after it was written.
</Tip>

## What the chain does not do

<AccordionGroup>
  <Accordion title="It does not prove a decision happened at a specific wall time">
    Timestamps are SDK-produced. A malicious writer with a valid API key could backdate a record. If you need non-repudiable time, add an external timestamping service such as RFC 3161 or a public blockchain anchor on top of the chain.
  </Accordion>

  <Accordion title="It does not prove the decision was correct">
    The chain proves a record exists as written and has not been altered since. Whether the decision itself was right or wrong is a business question — not something cryptographic integrity can answer.
  </Accordion>

  <Accordion title="It is not proof against key compromise">
    An attacker with write access to your API key can still insert records at the tip of the chain. The hash chain makes retroactive tampering detectable, not prospective injection. Rotate API keys promptly if you suspect a compromise.
  </Accordion>
</AccordionGroup>

For stronger guarantees — key-anchored signing and per-org RSA export signatures — see the forensic export flow in the compliance section.
