DocsFor developers
How Seal Check works
Every consent evidence export from ComplyZero is sealed with our signing key. Seal Check, and the free verifier your own team can run, re-check the seal, every file and every record without the company that sent the export. This page sets out exactly what they check, what a pass proves, and what it doesn't.
What an export contains
An export is a ZIP file, written byte for byte the same way every time, whose files appear in one fixed order, ending with manifest.json and manifest.sig.
| Export | Files, in order |
|---|---|
| Website consent records | consent-records.csv, presentation-payloads.json, purpose-versions.json, notice-versions.json, profile-revisions.json, rendering.json, manifest.json, manifest.sig |
| Person consent records | person-consent-records.csv, purpose-versions.json, form-versions.json, profile-revisions.json, rendering.json, manifest.json, manifest.sig |
| Offline evidence pack | forms.ndjson, submissions.ndjson, consents.ndjson, effects.ndjson, deliveries.ndjson, manifest.json, manifest.sig |
- Each record row carries every input of its record fingerprint (
record_digest, SHA-256 over the canonical JSONcanonical-json.v1of the record's stored facts: digest version 2 in website exports made after ComplyZero's October 2026 consent update, version 1 in earlier website exports and in person exports). JSON cells are canonical JSON. Contact details are never included in plain text. - The JSON files hold the exact versions the records cite: the purpose wording, the notice, the form, the privacy profile revision and the presentation each person was shown. With them, and with ComplyZero's renderer, the wording and presentation a person saw are rebuilt from the export alone. Seal Check and the verifier on its own report that step as not recomputed.
manifest.jsonlists the export's scope, filter, population counts, limitations, the file order, the digest version and canonicalisation, the versions cited, the key that signs it (signatureKid), and the SHA-256 and size of every other file.manifest.sigis{"alg": "Ed25519", "kid": ..., "signature": ...}in canonical JSON followed by a newline: an Ed25519 signature (base64url) over the exact bytes ofmanifest.json, made with the ComplyZero key named bykid, which must be the keymanifest.jsonnames.
What a pass proves, and what it doesn't
True under this design:
- Consent records are written once and can't be edited or deleted by the application, and every deletion at the end of a retention period is logged.
- Every record carries a SHA-256 fingerprint and, for a person, a sequence number.
- Exports are signed by ComplyZero and list every file's fingerprint, and Seal Check and the verifier re-check every record and the signature without the company that sent the export.
- The wording and presentation a person saw are rebuilt from the versioned artifacts the export includes. Disclosure fingerprints are recomputed only when the verifier is given ComplyZero's renderer.
Stated limits:
- A pass proves the export is what ComplyZero issued and that it is unchanged since, and it reveals records altered, removed or inserted inside the exported range.
- It doesn't prove that the database was complete or unaltered before the export, or when the export was issued.
- A sequence gap is something to investigate, not proof of who removed what. Nothing is proven about records after the last exported one: a record removed after the last exported one, a record added after the export, or a change made in the database before the export was built is not detected.
- An export signed with a key that was later revoked can't be told apart from one signed after the revocation, because the export carries no independent timestamp.
Not claimed: tamper-proof, court-proof, independently timestamped, or immune to ComplyZero itself.
The checks, in order
Seal Check and the verifier run the same checks in the same order, and reach the same result for the same file. They stop at the first failure of the first two.
- The signature.
manifest.sigagainst the keys they know, and the key the signedmanifest.jsonnames. An unknown key is reported asissuer unverified: key <kid> not in the pinned set; refresh online or obtain the key document, and a revoked key asissuer unverifiedwith the revocation date. In both cases the files and records are still checked and reported, the scope is marked as unverified, and the result is not a pass. - The layout and files. The file order against the export's type, and each file's SHA-256 and size against the manifest. Only the ZIP exactly as ComplyZero writes it is accepted: any other arrangement of the same files, such as a copy that was unzipped and zipped again, is reported as damaged.
- The records. Every
record_digestrecomputed from the row's own cells, using the manifest's digest version and canonicalisation. A digest version they don't know is reported as "unsupported digest version" and nothing more is claimed. - The sequences. Per profile in a person export, the sequence numbers present. A gap is reported only inside the exported range of an unfiltered export (a period alone is not a filter; a purpose or action filter is). Each person record's time is taken when its sequence number is assigned, so a period always holds a contiguous run of each profile's numbers.
- The versions. Every cited purpose, notice, form and profile version against the snapshot the export includes.
- The disclosures. Each presentation's disclosure fingerprint rebuilt from the included artifacts with ComplyZero's renderer. Without the renderer no row is recomputed: the line reads "Disclosure digests: disclosure references present; digest not recomputed by this verifier version." and the result can still be a pass, because the other checks passed and this one was not made.
- Expired evidence. The rows marked as expired evidence are counted.
Run the verifier yourself
Your own team can run the same checks without our website. You need Node.js 20 or later; the verifier uses nothing else. Download the verifier, then run:
node verify-evidence-package.mjs consent-export-<reference>.zip
Options:
--keys <file>: also trust the keys in a key document you downloaded (see The signing keys). Use this when you check without a connection.--refresh: fetch the current key document fromhttps://api.complyzero.com/.well-known/complyzero-evidence-keys.jsonbefore checking. A key document adds keys and can mark a key retired or revoked; it never replaces a key the verifier already pins.--keys-url <url>: fetch the key document from another address (implies--refresh).--renderer <file>: ComplyZero's renderer (notice-rendering.js), which the verifier needs to rebuild disclosure fingerprints. It isn't part of the download, so without it the disclosure step is reported as not recomputed and every other check still runs. Use a renderer at least as new as the export.
The report's third line, Package SHA-256, is the file's fingerprint: compare it with the ZIP fingerprint shown beside the export in ComplyZero, or with the one the sender gave you. Values the export supplies (file names, manifest fields, record cells) are printed with control and text-direction characters written as escapes, so an export can't change how the report reads.
Exit codes: 0 when the last line reads Result: verified.; 1 when a check failed (the last line names the first failure); 2 when the verifier could not run (no export named, the file can't be read, an unreadable key document, or a key set that names one public key under two key ids).
A passing website export, checked with ComplyZero's renderer, reads for example:
ComplyZero evidence package verifier, version 1
Package: consent-export-<reference>.zip
Package SHA-256: 9f2c...e41a.
Signature: verified (key evidence-202610).
Layout and files: 8 of 8 match.
Records: 4,812 verified, 0 failed.
Sequence gaps inside the exported range: not applicable (website records are not numbered per person).
Versions: 7 cited, 7 match.
Disclosure digests: 4,812 recomputed.
Expired evidence rows: 3.
What this check cannot establish: a sequence gap is an anomaly to investigate, not proof of who removed what; nothing is proven about records after the last exported one or about the database before the export.
The signature shows that ComplyZero issued the package and that it is unchanged since; it does not show when the package was issued.
Result: verified.
The report also prints the export's scope, filter, population counts and limitations between the signature and the layout lines.
The signing keys
The key document lists every evidence signing key ComplyZero has used, with kid, publicKey (SPKI PEM), validFrom, retiredAt and revokedAt. Keys are never removed from it, so every export stays checkable. The same document is served at https://privacy.complyzero.com/.well-known/complyzero-evidence-keys.json.
ComplyZero holds the signing key only in its production environment, never in the database, the source code or any message. A new key is added before the old one retires, and the key document keeps every key, so older exports still pass. If a key may have been exposed, ComplyZero revokes it from a stated date, and exports signed with it are then reported as "issuer unverified".