Guide
Automate FreeSign from the command line
FreeSign's read and verify surface is plain HTTP — you can script it end to end with curl and standard shell tools, no SDK required. This guide shows the copy-paste examples for headless and CI use, and is honest about the one part that does not run from a shell: the signing ceremony itself.
What runs headless, and what does not
FreeSign endpoints split cleanly in two:
| Surface | curl / shell? | Why |
|---|---|---|
| Local PDF hashing | Pure local shasum — nothing leaves the machine. | |
Hash lookup — GET /api/verify | Unauthenticated GET. | |
Receipts — GET /api/receipts/{id} | Unauthenticated GET. | |
| OpenTimestamps proof download | Unauthenticated GET. | |
Document-free MCP tools — POST /mcp | Plain JSON-RPC over HTTP. | |
| PDF verification (openssl / pyHanko / ots) | Offline tooling on the finished file. | |
The signing ceremony — /otp, /sign, /seal, /finalize | Each request must be signed by a non-extractable browser key — see the last section. |
Everything in the curl column below works against production (https://free-sign.com) or a local wrangler dev instance (http://localhost:8787). And the rule never changes: the PDF stays on your machine. FreeSign endpoints accept hashes and evidence only.
1. Hash the PDF locally
Every FreeSign workflow starts with the document's SHA-256, computed where the file lives:
# macOS
DOC_HASH=$(shasum -a 256 contract.pdf | cut -d' ' -f1)
# Linux
DOC_HASH=$(sha256sum contract.pdf | cut -d' ' -f1)
echo "$DOC_HASH"
That value is document_sha256 — a lowercase 64-character hex string. It is the only thing about the document FreeSign ever sees.
2. Check whether a hash is already on file
Look up any envelopes and statuses bound to a document hash. Useful in CI to detect “has this artifact already been signed?”
curl -s "https://free-sign.com/api/verify?document_sha256=$DOC_HASH" | jq
Returns the matching envelopes with their statuses (draft, otp_sent, signed, finalized, expired). An empty result means the hash is not known to FreeSign.
3. Drive the document-free MCP endpoint with curl
The MCP server at /mcp is a JSON-RPC endpoint — no MCP client library needed. List the tools:
curl -s https://free-sign.com/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'
Call verify_document_hash — the same lookup as step 2, via MCP:
curl -s https://free-sign.com/mcp \
-H 'content-type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"verify_document_hash\",\"arguments\":{\"document_sha256\":\"$DOC_HASH\"}}}" | jq
The document-free tool set is create_signing_envelope, verify_document_hash, get_receipt, and get_ots_proof. None of them accepts PDF bytes — the MCP discovery contract advertises documentUpload: false.
4. Pull a signing receipt
Once an envelope is signed, its full evidence is retrievable by id:
EID=env_1f3a...c90b
curl -s "https://free-sign.com/api/receipts/$EID" | jq
The response carries {envelope, receipts, ots_anchors} — the seal metadata, the final-payload signature, and the OpenTimestamps anchors. It contains evidence only, never PDF content. For what each field means, see the evidence JSON schema reference.
5. Download and verify the OpenTimestamps proof
Every seal is anchored to the OpenTimestamps public calendar pool. Pull the .ots proof and verify it independently:
RECEIPT=$(curl -s "https://free-sign.com/api/receipts/$EID")
ANCHOR_HASH=$(echo "$RECEIPT" | jq -r '.ots_anchors[0].anchored_hash')
PROOF_URL=$(echo "$RECEIPT" | jq -r '.ots_anchors[0].proof_download')
curl -s -o proof.ots "https://free-sign.com$PROOF_URL"
# Verify against the anchored hash (the signed PDF's signed-region digest)
ots verify --digest "$ANCHOR_HASH" proof.ots
# If still calendar-pending, upgrade then re-verify — works any time later
ots upgrade proof.ots
ots verify --digest "$ANCHOR_HASH" proof.ots
6. Verify a finished PDF headlessly
To validate a signed PDF in one pass — structure, CMS signature, certificate chain, timestamp, OpenTimestamps proof, and the embedded evidence JSON — run the project validator:
node tools/validate-sealed-pdf.mjs signed.pdf
For a step-by-step verification with openssl and pyHanko instead, follow Verify a signed PDF with openssl, pyHanko, and OpenTimestamps. None of these checks contacts a FreeSign server — the trust anchors live in the file.
Running the signing ceremony without a human at the keyboard
The protected endpoints — /otp, /otp/verify, /sign, /seal, /finalize — cannot be driven by plain curl. Each one must carry an ECDSA P-256 signature produced by a key that is generated at envelope-creation time and held non-extractably in the browser. A shell has no such key, so a bare curl call to a protected endpoint returns 401.
Two honest paths exist:
- Hand the ceremony to a browser. If an agent created the envelope (e.g. via the MCP
create_signing_envelopetool), openhttps://free-sign.com/?envelope=<envelope_id>&hash=<document_sha256>. The page binds a fresh session key, requires the locally selected PDF to match the hash, and runs the full ceremony — OTP, consent, local signing — in the browser. - Implement the session-signing in a real crypto runtime. A custom client (a Node CI agent, for instance) can generate the ECDSA P-256 keypair itself and sign each request. The wire format — the four
x-fsig-session-*headers and thecanonicalJsonpayload — is fully specified in /llms.txt under “Envelope-Scoped Session Binding”. That is beyond what a shell does cleanly, which is why this guide stops at the read surface.
One more thing, whichever path you take: automating the mechanics of a ceremony does not supply the authority to sign. An agent may operate a browser for a user, but it must not represent unattended automation as a human signature unless the human authorized that specific signing. See the llms.txt “Automation vs Signing Intent” section.
Building an integration?
The full REST surface is at /openapi.json, the agent skill at /free-sign-agent/SKILL.md. Or just sign one PDF by hand first.
Open free-sign.com →