← Back to API Documentation
Technical Note
Developer Reference

EVIDE Payload Canonicalization

Deterministic JSON Normalization for Evidentiary Integrity
Specification v1.0 · May 2026 · EVIDE Schema 2.0
This document specifies the canonicalization algorithm applied to all EVIDE intake payloads before SHA-256 hashing. Implementing this algorithm on the source side enables pre-intake hash verification and extends the integrity chain to cover the full transit.
Section 1
1. Why Canonicalization

JSON does not guarantee key ordering. Two semantically identical JSON objects can produce different byte sequences — and therefore different SHA-256 hashes — if keys are serialized in a different order.

"Two semantically identical EVIDE payloads must always produce the same SHA-256 hash,
regardless of the order in which keys were submitted."

Without canonicalization, hash verification would be fragile: a developer rebuilding the payload from their system's internal representation in a different key order would obtain a different hash, making integrity verification impossible without the original byte stream.

Canonicalization solves this by defining a single deterministic normalization step that all implementations must apply before computing the hash. The result is that the hash becomes a property of the semantic content of the payload, not of its serialization order.

The canonicalization step is applied server-side before storage. The intake_hash returned in the API response is always the hash of the canonicalized form. Source systems implementing the same algorithm can compute the expected hash before submission and verify it against the returned value.
Section 2
2. The Algorithm

The EVIDE canonicalization algorithm consists of four steps applied in sequence:

1
Remove content_hash
If the payload contains a top-level content_hash field, remove it before any further processing. This field is a source-side pre-transit hash declaration — it must not be included in the canonical form, as doing so would create a circular dependency.
2
Recursive key sort
Sort all keys alphabetically at every nesting level of the JSON tree. This applies to all objects, including nested objects within arrays. Array element order is preserved — only object keys are sorted. The sort is case-sensitive and uses standard lexicographic ordering over UTF-8 compatible key strings.
3
Minified serialization
Serialize to compact JSON with no whitespace between tokens. Unicode characters must be preserved as-is (no \uXXXX escape sequences for non-ASCII characters). Forward slashes must not be escaped (/ not \/). The output encoding is UTF-8.
4
SHA-256 hash
Compute SHA-256 of the UTF-8 byte string produced in step 3. Output as lowercase hexadecimal. The result is the EVIDE intake hash.
Serialization flags (PHP reference):
json_encode($canonical, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)

These two flags are required. Without JSON_UNESCAPED_UNICODE, multi-byte characters are escaped as \uXXXX sequences, producing a different byte string. Without JSON_UNESCAPED_SLASHES, forward slashes in URLs are escaped as \/, again producing a different byte string.
Section 3
3. Key Ordering Reference

The following tables show the alphabetical key order for the most relevant objects after canonicalization. These orderings are the result of standard lexicographic sort and are provided here as a reference to avoid implementation errors in common nested objects.

Top-level keys (partial)
#Key
1authority
2chain
3created_at_utc
4decision
5evide_schema
6fedis_requested
7handoff
8human_oversight
9intervention
10object_class
11source_reference
12source_system
13source_timestamp_utc
classification_context keys
#Key
1taxonomy_reference
2threshold_authority
3threshold_reference
4threshold_status
handoff.boundary_readiness keys (v2.0)
#Key
1readiness_gate
2status
3unresolved_signals
4visibility_surface
Verify your implementation: if your computed hash matches the intake_hash returned by the API, the canonicalization is correct. Run this check on a known payload immediately after integration.
Section 4
4. Worked Example

The following example shows the same payload before and after canonicalization. The canonical form is what EVIDE hashes.

Input payload (as submitted — keys in arbitrary order)
"source_system": "AquariuOS", "evide_schema": "2.0", "authority": { "role": "HR Reviewer", "id": "user_87421" }, "decision": { "summary": "Override applied", "status": "finalized", "type": "candidate_evaluation", "closure_timestamp_utc": "2026-04-07T09:15:00Z" }, "source_reference": "CDR-2026-00421", "source_timestamp_utc": "2026-04-07T09:15:00Z", "content_hash": "abc123..." ← removed before hashing
After canonicalization (step 1: remove content_hash · step 2: sort keys · step 3: minify)
{"authority":{"id":"user_87421","role":"HR Reviewer"},"decision":{"closure_timestamp_utc":"2026-04-07T09:15:00Z","status":"finalized","summary":"Override applied","type":"candidate_evaluation"},"evide_schema":"2.0","source_reference":"CDR-2026-00421","source_system":"AquariuOS","source_timestamp_utc":"2026-04-07T09:15:00Z"}
SHA-256 of the canonical string above
sha256("{"authority":{"id":"user_87421",...}") → 03eb8dbb1c3bf7abeed8896fef1a7d7182fbc4f814286cca0aa427c81dc66daa
The hash above is illustrative. The actual hash depends on the complete payload. Run the algorithm on your full payload and compare against the intake_hash returned by the API.
Section 5
5. Language Implementations

Reference implementations of the EVIDE canonicalization algorithm. All three produce identical output for identical input.

PHP 8.1+
function evide_canonical_sort(mixed $data): mixed { if (!is_array($data)) return $data; // Distinguish JSON arrays from JSON objects $isList = array_is_list($data); if ($isList) { return array_map('evide_canonical_sort', $data); } ksort($data, SORT_STRING); foreach ($data as $k => $v) { $data[$k] = evide_canonical_sort($v); } return $data; } function evide_intake_hash(array $payload): string { unset($payload['content_hash']); $canonical = evide_canonical_sort($payload); $json = json_encode($canonical, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); return hash('sha256', $json); }
Python 3.x
import json, hashlib def canonical_sort(data): if isinstance(data, dict): return {k: canonical_sort(v) for k, v in sorted(data.items())} if isinstance(data, list): return [canonical_sort(v) for v in data] return data def evide_intake_hash(payload: dict) -> str: payload = {k: v for k, v in payload.items() if k != 'content_hash'} canonical = canonical_sort(payload) json_str = json.dumps(canonical, separators=(',', ':'), ensure_ascii=False) return hashlib.sha256(json_str.encode('utf-8')).hexdigest()
JavaScript (Node.js / browser)
function canonicalSort(data) { if (Array.isArray(data)) { return data.map(canonicalSort); } if (typeof data !== 'object' || data === null) { return data; } return Object.keys(data).sort().reduce((acc, k) => { acc[k] = canonicalSort(data[k]); return acc; }, {}); } // Node.js (crypto module) const crypto = require('crypto'); async function evideIntakeHash(payload) { const { content_hash, ...rest } = payload; const canonical = canonicalSort(rest); const jsonStr = JSON.stringify(canonical); return crypto.createHash('sha256').update(jsonStr, 'utf8').digest('hex'); } // Browser (Web Crypto API) async function evideIntakeHashBrowser(payload) { const { content_hash, ...rest } = payload; const canonical = canonicalSort(rest); const jsonStr = JSON.stringify(canonical); const buf = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(jsonStr) ); return Array.from(new Uint8Array(buf)) .map(b => b.toString(16).padStart(2, '0')).join(''); }
JavaScript note: JSON.stringify in all major JS engines does not escape forward slashes or non-ASCII characters by default, which is the correct behavior for EVIDE canonicalization. No additional flags are needed in JavaScript, unlike PHP.
Section 6
6. The Integrity Chain

The EVIDE integrity model consists of two complementary hash layers:

  • content_hash (optional, source-declared) — the SHA-256 of the canonical payload computed by the source system before submission. If present, it extends the integrity chain to cover the full pre-intake transit: the source system commits to the payload content before the API ever receives it.
  • intake_hash (mandatory, server-computed) — the SHA-256 of the canonical payload as received and stored by EVIDE. This is the authoritative fingerprint. It is returned in the API response and stored independently of the payload.
Verification guarantee: if content_hash == intake_hash, the payload was not modified between source canonicalization and EVIDE intake. If they differ, at least one of: the payload was modified in transit, a different canonicalization was used on the source side, or content_hash was computed on the non-canonical form.

For FEDIS certification, the intake_hash is the value included in the forensic artifact. The source-declared content_hash is an optional integrity extension — useful for chain-of-custody documentation but not required for FEDIS issuance.

Implementation checklist:
✓ Remove content_hash before sorting
✓ Sort keys recursively at every nesting level
✓ Minify with no whitespace
✓ Preserve Unicode characters as-is (no \uXXXX escape)
✓ Do not escape forward slashes
✓ Encode the string as UTF-8 before hashing
✓ Compare your output against the returned intake_hash

References

EVIDE Minimum Intake JSON — Schema Reference: app.certifywebcontent.com/docs/evide-intake-schema/

Live JSON Schema: app.certifywebcontent.com/json

FEDIS Reference: certifywebcontent.com — FEDIS

EVIDE Signals & Roadmap: app.certifywebcontent.com/signals

Canonicalization in one sentence
Sort. Minify. Hash.
The same content always produces the same fingerprint.
Questions or integration support: info@informaticainazienda.it