Reference
Evidence JSON schema
Every FreeSign signature carries a structured evidence record. OTP signers produce the free-sign.com/evidence/v1 JSON; passkey (WebAuthn) signers produce free-sign.com/evidence/v2, a strict superset of v1. This page documents every field, what each one proves, and how to validate the record against the machine-readable schema. It is the authoritative reference for the embedded evidence format.
Where the evidence JSON lives
The evidence JSON is not a separate download. It is embedded inside the signed PDF, in the signature's CMS (PKCS#7) container as an unsignedAttribute with OID 1.3.6.1.4.1.65834.1.2 — under FreeSign's IANA Private Enterprise Number 65834. The attribute value is an OCTET STRING wrapping the UTF-8 JSON bytes.
Because the record lives inside each signer's own CMS — wholly within their own signed PDF revision — a multi-signer PDF carries every signer's evidence, signer #2 included. A PDF file attachment could not do that without breaking earlier signatures, which is why the CMS unsignedAttribute was chosen. Extract it with any CMS parser: the /verify page, tools/validate-sealed-pdf.mjs, or openssl cms (see extraction below).
The two halves of the evidence bundle
The signing ceremony produces evidence in two parts. Only the first is the embedded JSON this page specifies.
| Half | Where it is | What it covers |
|---|---|---|
| Pre-seal — the evidence JSON | Embedded in the PDF, CMS unsignedAttribute 1.3.6.1.4.1.65834.1.2 |
Signer identity, OTP, consent, the signed canonical_payload + its signature + the browser public key. Everything that exists before the cryptographic seal. |
| Seal + finalize | The CMS itself, plus GET /api/receipts/{envelope_id} |
CMS signature, leaf + CA certificate chain, RFC 3161 timestamp, OpenTimestamps proof, final_pdf_sha256, final_payload, final_signature. These describe (or sign) the finished bytes, so they cannot live inside those bytes. |
Machine-readable schema
A JSON Schema (draft 2020-12) for the embedded evidence JSON is published at a stable URL:
https://free-sign.com/evidence/v1/schema.json
Validate an extracted record against it with any standard validator:
# Python (jsonschema / check-jsonschema)
pipx run check-jsonschema --schemafile https://free-sign.com/evidence/v1/schema.json evidence.json
# Node (ajv-cli)
npx ajv-cli validate -s schema.json -d evidence.json --spec=draft2020
That URL is the v1 schema, used by signers who verified with an emailed one-time code. Signers who used a passkey instead produce a v2 record — a strict superset of v1 — published at a parallel URL:
https://free-sign.com/evidence/v2/schema.json
Tell the two apart by the schema field ("free-sign.com/evidence/v1" vs "…/v2") or by the identity_method field, which only v2 carries. The fields below describe v1; the Passkey signers section lists what v2 adds.
Top-level fields
The embedded evidence JSON is a flat object with the following members. Hashes are lowercase hexadecimal SHA-256 unless noted.
| Field | Type | What it is / proves |
|---|---|---|
schema | string | Constant "free-sign.com/evidence/v1". Pins the record to this version. |
envelope_id | env_ + 32 hex | The signing envelope this record belongs to. |
document_sha256 | hex (64) | SHA-256 of the original PDF bytes, before any FreeSign stamping. The hash the signer committed to. |
signer_name | string | The signer's typed legal name. Also the leaf certificate Subject CN and the /Sig /Name field. |
signer_email | string | The OTP-verified signer email, in plaintext. Present because the signer's own PDF already carries it in the leaf cert subjectAltName and the /Sig /ContactInfo field. FreeSign servers store only the HMAC form. |
signer_email_hmac | hex (64) | Envelope-scoped HMAC-SHA-256 of the signer email. The only form of the email the FreeSign database retains. |
otp_challenge_id | otp_ + 32 hex | The one-time-code challenge verified before signing. |
otp_verified_at | ISO-8601 UTC | When the OTP challenge was verified. |
payload_hash | hex (64) | SHA-256 of canonicalJson(canonical_payload). Recompute and compare to detect tampering of the consent payload. |
consent_version | string | Label of the consent text the signer accepted. |
consent_text_sha256 | hex (64) | SHA-256 of the exact consent text shown to the signer, served by GET /api/consent. |
document_viewed_at | ISO-8601 UTC, or null | When the signer's browser first rendered the PDF preview. |
request_fingerprint | object, or null | Connecting IP, forwarded headers, user-agent, and Cloudflare edge metadata captured at /sign — see below. |
created_at | ISO-8601 UTC | Server clock at POST /sign. |
canonical_payload | object | Exactly the object the browser ECDSA-signed — see below. |
signature_base64url | string | base64url ECDSA P-256 / SHA-256 signature over canonicalJson(canonical_payload). The primary signature. |
public_key_jwk | object | The signer's ECDSA P-256 public key — see below. |
receipt_id | rcp_ + 32 hex | The signature receipt this ceremony produced (one per signer). |
note | string | Human-readable provenance note. Informational only. |
canonical_payload — the signed object
This nested object is exactly what the signer's browser signed with its ECDSA P-256 key. Field order in the file is irrelevant: both signer and verifier re-serialize it through canonicalJson() (recursively key-sorted, no whitespace, UTF-8) before hashing. The set of keys is frozen for v1 — adding or removing any field changes signature_base64url.
| Field | Type | What it is |
|---|---|---|
app | string | Constant "free-sign.com". Scopes the signature to FreeSign. |
envelope_id | env_ + 32 hex | The envelope being signed. |
document_sha256 | hex (64) | Original PDF hash — equals the top-level document_sha256. |
email_hmac | hex (64) | Envelope-scoped HMAC of the signer email — equals signer_email_hmac. |
signer_name | string | The signer's typed legal name. |
otp_challenge_id | otp_ + 32 hex | The verified OTP challenge. |
otp_verified_at | ISO-8601 UTC | When the OTP was verified. |
consent_version | string | Consent text label. |
consent_text_sha256 | hex (64) | SHA-256 of the consent text. |
document_viewed_at | ISO-8601 UTC | Browser preview-render time (signing time is substituted if no view was recorded). |
signed_at | ISO-8601 UTC | Browser clock at the moment of signing. |
user_agent_sha256 | hex (64) | SHA-256 of the signer's browser User-Agent string — a hashed fingerprint, not the raw UA. |
consent_accepted is sent as a separate field in the /sign request body, but it is not part of canonical_payload — do not expect it inside the signed object.
Passkey signers — evidence v2
When a signer authenticates with a passkey (WebAuthn — Face ID, Touch ID, Windows Hello, or a security key) instead of an emailed code, the evidence record is free-sign.com/evidence/v2. It is a strict superset of v1 with three differences:
- The
schemafield is"free-sign.com/evidence/v2", and a new top-levelidentity_methodfield is"passkey"(an OTP record reports"otp"). - The OTP fields
otp_challenge_idandotp_verified_atare absent — both at the top level and insidecanonical_payload. In their placecanonical_payloadcarries awebauthnobject holding thecredential_idof the passkey used. Exactly one identity proof is ever present: the OTP fields or thewebauthnobject, never both. - A new top-level
webauthn_assertionobject records the hardware-backed proof:credential_id,authenticator_data,client_data_json,signature,cose_public_key,aaguid,sign_count, andsign_count_regressed. A verifier re-checks this signature against its COSE key and confirms the WebAuthn challenge equalspayload_hash— the /verify page andtools/validate-sealed-pdf.mjsboth do this offline.
Everything else — the seal, the CMS, the certificate chain, the timestamp — is identical. The passkey is an extra identity-and-intent layer, not a replacement for the per-user signing certificate. Validate a v2 record against /evidence/v2/schema.json; v1 stays frozen for OTP records.
public_key_jwk
The signer's ephemeral browser ECDSA P-256 public key, as exported by WebCrypto. The matching private key is non-extractable and is gone once the signing tab closes. This one public key verifies both the embedded signature_base64url and the receipts-only final_signature_base64url — so a verifier needs no extra key material.
{
"kty": "EC",
"crv": "P-256",
"x": "<base64url, 43 chars>",
"y": "<base64url, 43 chars>"
}
WebCrypto may also emit ext and key_ops members; they carry no signing significance.
request_fingerprint
Request metadata captured server-side at POST /sign — the same signing-act evidence DocuSign and Adobe Sign retain on a signing event. FreeSign's privacy invariant is “no document bytes server-side”, not “no IPs”. Cloudflare-only fields (cf) are null in local development. Notable members: cf_connecting_ip, x_forwarded_for, user_agent, accept_language, referer, the sec_ch_ua* client hints, a cf object (country, city, ASN, colo, TLS version), and captured_at. The same object is folded into every audit-chain row.
Worked example
An abridged evidence record (hashes truncated for readability):
{
"schema": "free-sign.com/evidence/v1",
"envelope_id": "env_1f3a...c90b",
"document_sha256": "9b2c...e41a",
"signer_name": "Ada Lovelace",
"signer_email": "ada@example.com",
"signer_email_hmac": "7d10...44ff",
"otp_challenge_id": "otp_5e8d...2a17",
"otp_verified_at": "2026-05-20T09:14:02Z",
"payload_hash": "c4a9...80be",
"consent_version": "esign-ueta-eidas-ades-2026-05",
"consent_text_sha256": "2f6b...d3c1",
"document_viewed_at": "2026-05-20T09:13:40Z",
"request_fingerprint": { "cf_connecting_ip": "203.0.113.7", "user_agent": "Mozilla/5.0 ...", "cf": { "country": "PL" }, "captured_at": "2026-05-20T09:14:05Z" },
"created_at": "2026-05-20T09:14:05Z",
"canonical_payload": {
"app": "free-sign.com",
"envelope_id": "env_1f3a...c90b",
"document_sha256": "9b2c...e41a",
"email_hmac": "7d10...44ff",
"signer_name": "Ada Lovelace",
"otp_challenge_id": "otp_5e8d...2a17",
"otp_verified_at": "2026-05-20T09:14:02Z",
"consent_version": "esign-ueta-eidas-ades-2026-05",
"consent_text_sha256": "2f6b...d3c1",
"document_viewed_at": "2026-05-20T09:13:40Z",
"signed_at": "2026-05-20T09:14:04Z",
"user_agent_sha256": "a17c...9e22"
},
"signature_base64url": "MEUCIQ...rA",
"public_key_jwk": { "kty": "EC", "crv": "P-256", "x": "f8K...", "y": "Qd2..." },
"receipt_id": "rcp_8c4b...10ad",
"note": "FreeSign signing evidence, embedded in the signed PDF's CMS ..."
}
How to extract the record from a signed PDF
The evidence JSON is inside the CMS, not loose in the file. Three ways to pull it out:
- The /verify page — drop the PDF on free-sign.com/verify; it parses the CMS in the browser and shows the record.
- The project validator —
node tools/validate-sealed-pdf.mjs signed.pdfwrites the record to/tmp/_validate.evidence.json(step [6]). - openssl — extract the CMS, then read the
1.3.6.1.4.1.65834.1.2unsignedAttribute withopenssl asn1parse.
The seal + finalize half (receipts-only)
These fields are not in the embedded JSON — they describe the finished bytes. Retrieve them from GET /api/receipts/{envelope_id}:
seal—cms_sha256,cert_sha256(the leaf cert),signer_cert_serial_hex,signer_cert_not_after,seal_ca_mode,seal_profile(PAdES-B-BorPAdES-B-T),signed_at.ots_anchor— the OpenTimestamps anchor:anchored_hash(the signed-region digest),status(pending/confirmed/deferred),embedded_in_cms, calendar URLs, and once upgraded, the Bitcoin block height + hash.final_pdf_sha256— SHA-256 of the fully assembled signed PDF. A file cannot carry its own hash, so you compute this directly:shasum -a 256 signed.pdf.final_payload+final_signature_base64url— a second ECDSA signature, by the same browser key, over{app, envelope_id, document_sha256, final_pdf_sha256, payload_hash, finalized_at}.
For the full end-to-end verification walkthrough, see Verify a signed PDF with openssl, pyHanko, and OpenTimestamps. For the AI-facing version of this schema, see /llms.txt.
Want a record to inspect?
Sign a test PDF in your browser, then extract its evidence JSON. No account required.
Sign a PDF now →