FreeSign

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.

Pre-seal half vs seal/finalize half
HalfWhere it isWhat 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.

Members of the free-sign.com/evidence/v1 object
FieldTypeWhat it is / proves
schemastringConstant "free-sign.com/evidence/v1". Pins the record to this version.
envelope_idenv_ + 32 hexThe signing envelope this record belongs to.
document_sha256hex (64)SHA-256 of the original PDF bytes, before any FreeSign stamping. The hash the signer committed to.
signer_namestringThe signer's typed legal name. Also the leaf certificate Subject CN and the /Sig /Name field.
signer_emailstringThe 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_hmachex (64)Envelope-scoped HMAC-SHA-256 of the signer email. The only form of the email the FreeSign database retains.
otp_challenge_idotp_ + 32 hexThe one-time-code challenge verified before signing.
otp_verified_atISO-8601 UTCWhen the OTP challenge was verified.
payload_hashhex (64)SHA-256 of canonicalJson(canonical_payload). Recompute and compare to detect tampering of the consent payload.
consent_versionstringLabel of the consent text the signer accepted.
consent_text_sha256hex (64)SHA-256 of the exact consent text shown to the signer, served by GET /api/consent.
document_viewed_atISO-8601 UTC, or nullWhen the signer's browser first rendered the PDF preview.
request_fingerprintobject, or nullConnecting IP, forwarded headers, user-agent, and Cloudflare edge metadata captured at /sign — see below.
created_atISO-8601 UTCServer clock at POST /sign.
canonical_payloadobjectExactly the object the browser ECDSA-signed — see below.
signature_base64urlstringbase64url ECDSA P-256 / SHA-256 signature over canonicalJson(canonical_payload). The primary signature.
public_key_jwkobjectThe signer's ECDSA P-256 public key — see below.
receipt_idrcp_ + 32 hexThe signature receipt this ceremony produced (one per signer).
notestringHuman-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.

Members of canonical_payload
FieldTypeWhat it is
appstringConstant "free-sign.com". Scopes the signature to FreeSign.
envelope_idenv_ + 32 hexThe envelope being signed.
document_sha256hex (64)Original PDF hash — equals the top-level document_sha256.
email_hmachex (64)Envelope-scoped HMAC of the signer email — equals signer_email_hmac.
signer_namestringThe signer's typed legal name.
otp_challenge_idotp_ + 32 hexThe verified OTP challenge.
otp_verified_atISO-8601 UTCWhen the OTP was verified.
consent_versionstringConsent text label.
consent_text_sha256hex (64)SHA-256 of the consent text.
document_viewed_atISO-8601 UTCBrowser preview-render time (signing time is substituted if no view was recorded).
signed_atISO-8601 UTCBrowser clock at the moment of signing.
user_agent_sha256hex (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:

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

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 →