Guide
Embed FreeSign signing on your site
Banks, insurers, and internal portals often need a signing step inside their own UI — without sending the contract PDF to a third-party vault. FreeSign’s pure-embed v1 integration loads a framable ceremony at /embed, ships a dependency-free SDK at /freesign-embed.js, and moves the PDF between your page and the iframe with a versioned postMessage contract. The server path is unchanged: hashes and evidence only, never document bytes.
What stays the same
- Privacy invariant: FreeSign still never receives PDF bytes. Your parent page passes the file to the iframe in the browser; the Worker sees the same SHA-256 and session-signed API calls as on free-sign.com.
- Output: the same PAdES-B-T signed PDF, per-user leaf cert, DigiCert RFC 3161 timestamp, OpenTimestamps proof, and evidence JSON embedded in the CMS.
- Identity: OTP (and optional passkeys) inside the iframe; no API keys or webhooks in v1.
Architecture at a glance
Your site hosts the PDF and the integration shell. FreeSign hosts the ceremony UI and the signing API. The only bridge is postMessage with freesign: "1" (full tables in /llms.txt under “Embedded signing” and in the repo’s docs/EMBED-PROTOCOL.md).
- Parent loads
/freesign-embed.jsand callsnew FreeSignSigner({...}). - SDK inserts an
<iframe src="/embed">(useorigin: "https://free-sign.com"in production, or your dev host when testing locally). - Iframe emits
ready; SDK sendsloadwith the PDF (recommended max 25 MiB). - Signer completes email, OTP/passkey, and consent inside the frame.
- Iframe emits
signed; youronSignedcallback receivessignedPdfasUint8Array.
Step-by-step
1. Load the SDK
<div id="signing-host"></div>
<script src="https://free-sign.com/freesign-embed.js"></script>
The script is standalone (no bundler). It sets iframe.allow for WebAuthn delegation when the browser supports passkeys in embedded frames.
2. Mount FreeSignSigner
const signer = new FreeSignSigner({
container: "#signing-host",
pdf: pdfBytes, // ArrayBuffer or Uint8Array from your app
filename: "contract.pdf",
signer: { email: "you@example.com", name: "Ada Lovelace" }, // optional prefill
language: "pl", // optional; defaults to the signer's browser language
branding: { // optional; recolour + logo (cosmetic; via the SDK it is sent only over postMessage, never to the server)
bg: "#eef4fb", text: "#0b1f3a", accent: "#1d4ed8",
brandName: "ACME Corp",
logo: "data:image/svg+xml;base64,…",
},
origin: "https://free-sign.com",
onSigned({ signedPdf, envelopeId, evidenceJson, verificationUrl, documents, failed, total }) {
// single document: use the flat fields. batch (pass `pdfs`): iterate
// documents[] (succeeded) and inspect failed[]/total — see below.
// persist signedPdf on your origin
},
onError({ code, message }) {
console.error(code, message);
},
});
onSigned and onError are required. Optional hooks: onReady, onPdfLoaded, onOtpSent, onCancelled, onNotice (non-terminal advisories such as an over-cap batch trimmed to the limit, or a dismissed passkey falling back to OTP — the ceremony keeps going).
language is optional and controls the iframe ceremony copy only. Use a primary language code such as pl or a browser-style tag such as pl-PL. If omitted, FreeSign uses the signer’s browser language and falls back to English; unsupported values also fall back to English.
branding is optional and purely cosmetic — it never changes hashes, evidence, certificates, or verification. Pass hex colours for bg/text/accent, a short brandName, and a logo as a data: image URI or a same-origin path (external image origins are out of scope in v1). With the SDK, branding travels only over the load postMessage and never reaches FreeSign servers. You can alternatively set the colour/name subset as query params on the iframe, e.g. /embed?bg=%23eef4fb&accent=%231d4ed8&brandName=ACME, which applies the theme at first paint — but those params are part of the GET URL and are therefore visible to the server (the logo is never read from the URL). Invalid values are dropped and fall back to the default theme.
Multi-document batch signing
To sign several PDFs in one ceremony, pass pdfs instead of pdf. One emailed OTP code (or one passkey prompt per document) unlocks the whole batch, and the iframe shows a clickable document list with a per-document ✓/✗ status.
const signer = new FreeSignSigner({
container: "#signing-host",
pdfs: [
{ pdf: bytesA, filename: "nda.pdf" },
{ pdf: bytesB, filename: "msa.pdf" },
], // up to BATCH_MAX_DOCS (default 10); each ≤25 MiB
onSigned({ documents, failed, total }) {
// documents[] = succeeded, failed[] = [{ filename, error }], total = batch size
for (const doc of documents) persist(doc.signedPdf, doc);
if (failed.length) console.warn(`${failed.length} of ${total} documents failed`, failed);
},
onError({ code, message }) { console.error(code, message); },
});
For a multi-document batch, onSigned receives documents (an array of the succeeded documents, each with signedPdf, filename, envelopeId, evidenceJson, verificationUrl, …), plus failed ([{ filename, error }], empty on a clean batch) and total (the batch size). For a single document the legacy flat fields (signedPdf, envelopeId, …) are still present at the top level, so existing one-PDF integrations keep working unchanged. A partial batch still fires onSigned with the successes; only a fully failed batch withholds it (you get per-document onError events instead).
3. Protocol and origin pinning
Every message includes freesign: "1". The iframe pins your page on the first parent load and replies only to that origin thereafter (signed PDF is never broadcast with targetOrigin: "*"). The SDK filters inbound events to event.source === iframe.contentWindow and your configured origin.
Stable error codes include pdf_too_large, pdf_invalid, envelope_failed, and ceremony_failed. User dismissals emit cancelled with otp_dismissed or passkey_dismissed.
4. Ceremony inside the iframe
The embedded surface reuses the same seal pipeline as the home page (embed.js + shared ceremony-core.js). Turnstile, when enabled on the deployment, gates OTP delivery the same way. Self-verify runs before the iframe posts signed back to you.
5. After signing
Store signedPdf in your backend or DMS. Use verificationUrl for a human-readable receipt link. Call signer.reset() to run another document in the same iframe, or signer.destroy() to tear down.
Security headers (for your security review)
/embedis intentionally framable (frame-ancestors *in v1). Completing a signature still requires the signer’s OTP or passkey in the frame./freesign-embed.jsis served withCross-Origin-Resource-Policy: cross-originso your origin can load it.- There is no domain allowlist in v1; a future API-key stage can tighten
frame-ancestorsper institution.
Out of scope in v1
No integration API keys, server webhooks, or envelope resume via URL parameters inside the iframe. Agents automating signing should use REST/MCP on hashes, not this embed path — see headless automation and /free-sign-agent/SKILL.md.
Try it interactively
The parent-page demo loads a real iframe, streams a local PDF through the SDK, and logs every postMessage event. Use it to validate origin pinning and the onSigned callback before wiring your own backend.
Open the embedded signing demo →