Verification API
Verify signed documents programmatically. Look up by code, upload a PDF, query the transparency log, or verify entirely offline using embedded W3C credentials.
How Verification Works
Every document signed through SignForge embeds cryptographic proof directly inside the PDF. Verification uses a multi-layer cascade — each layer is self-sufficient, so verification works even if some layers are unavailable.
W3C VC
Validate the embedded Verifiable Credential with ECDSA P-256
DB Lookup
Match verification code against the signing database
Hash Lookup
Match document SHA-256 against the transparency log
ECDSA Fallback
Verify embedded JSON signature without any database
Endpoints
All verification endpoints are public and require no API key. Rate limited to prevent abuse.
Document Verification
/api/verify/{code}/api/verify/uploadTransparency Log
/api/transparency/latest/api/transparency/hash/{sha256}/api/transparency/entry/{id}/api/transparency/keysDID Discovery
/.well-known/did.json24h cacheVerify by Code
Every signed PDF includes a verification code (printed on the document and embedded in the QR code). The code format is sf_ followed by 12 alphanumeric characters.
curl https://signforge.io/api/verify/sf_abc123def456Response
{
"verified": true,
"verification_code": "sf_abc123def456",
"envelope_title": "Service Agreement",
"sender": {
"name": "Alex Chen",
"email": "alex@company.com"
},
"signer": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"signed_at": "2026-03-25T11:59:00Z",
"original_sha256": "a1b2c3...",
"signed_sha256": "d4e5f6...",
"page_count": 3,
"document_size_bytes": 245760,
"hash_match": true,
"method": "verifiable_credential",
"vc_valid": true,
"transparency_log": {
"entry_id": 142,
"merkle_root": "7f8e9d...",
"merkle_proof": ["a1b2c3...", "d4e5f6..."],
"signed_tree_head": "eyJhbGci...",
"logged_at": "2026-03-25T12:00:00Z"
}
}method indicates which verification layer confirmed the document: verifiable_credential, embedded_json, hash_lookup, or cryptographic_signature.
Verify by Upload
Upload a signed PDF and the API will extract the embedded proof, verify the cryptographic signatures, and check the transparency log. Supports files up to 15 MB.
Python
import requests
with open("signed_contract.pdf", "rb") as f:
resp = requests.post(
"https://signforge.io/api/verify/upload",
files={"file": ("signed_contract.pdf", f, "application/pdf")},
)
result = resp.json()
print("Verified:", result["verified"])
print("Method:", result["method"])
print("VC Valid:", result["vc_valid"])
if result.get("transparency_log"):
log = result["transparency_log"]
print(f"Log entry #{log['entry_id']}, root: {log['merkle_root'][:16]}...")Node.js
const fs = require("fs");
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("signed_contract.pdf")]), "signed_contract.pdf");
const resp = await fetch("https://signforge.io/api/verify/upload", {
method: "POST",
body: form,
});
const result = await resp.json();
console.log("Verified:", result.verified);
console.log("Method:", result.method);cURL
curl -X POST https://signforge.io/api/verify/upload \
-F "file=@signed_contract.pdf"Verify by Document Hash
If you have the SHA-256 hash of a signed PDF, you can look it up directly in the transparency log without uploading the file.
# Compute the SHA-256 hash of the signed PDF
HASH=$(sha256sum signed_contract.pdf | cut -d' ' -f1)
# Look up in the transparency log
curl https://signforge.io/api/transparency/hash/$HASHResponse
{
"entry_id": 142,
"document_hash": "d4e5f6a1b2c3...",
"envelope_id": "550e8400-e29b-41d4-a716-446655440000",
"verification_code": "sf_abc123def456",
"merkle_root": "7f8e9d...",
"merkle_proof": ["a1b2c3...", "d4e5f6..."],
"signed_tree_head": "eyJhbGci...",
"created_at": "2026-03-25T12:00:00Z"
}Offline Verification
The showpiece: no network required
Every signed PDF embeds a W3C Verifiable Credential, ECDSA signatures, and public keys. This script extracts and verifies them entirely offline — no SignForge account, no internet connection, no specific PDF viewer.
Python (PyMuPDF + cryptography)
"""Offline verification of a SignForge-signed PDF.
Requires: pip install PyMuPDF cryptography base58
"""
import fitz # PyMuPDF
import json, hashlib, base64
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
import base58
def base64url_decode(s: str) -> bytes:
"""Decode base64url (no padding) to bytes."""
s += "=" * (4 - len(s) % 4)
return base64.urlsafe_b64decode(s)
def jcs_canonicalize(obj: dict) -> str:
"""JCS canonicalization (RFC 8785) — sorted keys, compact, UTF-8."""
return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def verify_offline(pdf_path: str) -> dict:
doc = fitz.open(pdf_path)
# 1. Extract embedded files from the signed PDF
vc_data = None
keys_data = None
for name in doc.embfile_names():
if name == "signforge_receipt.vc.json":
vc_data = json.loads(doc.embfile_get(name))
elif name == "signforge_keys.json":
keys_data = json.loads(doc.embfile_get(name))
doc.close()
if not vc_data or not keys_data:
return {"verified": False, "reason": "No embedded proof found"}
# 2. Extract and decode the ECDSA signature (base58btc with 'z' prefix)
proof = vc_data.get("proof", {})
if proof.get("type") != "DataIntegrityProof":
return {"verified": False, "reason": "Unsupported proof type"}
proof_value = proof.get("proofValue", "")
sig_bytes = base58.b58decode(proof_value[1:]) # strip 'z' multibase prefix
# 3. Rebuild canonical hashes (JCS = sorted JSON, compact separators)
# Order matters: options_hash FIRST, then vc_hash
vc_without_proof = {k: v for k, v in vc_data.items() if k != "proof"}
vc_canonical = jcs_canonicalize(vc_without_proof)
vc_hash = hashlib.sha256(vc_canonical.encode("utf-8")).digest()
proof_options = {k: v for k, v in proof.items() if k != "proofValue"}
opts_canonical = jcs_canonicalize(proof_options)
opts_hash = hashlib.sha256(opts_canonical.encode("utf-8")).digest()
combined = opts_hash + vc_hash # 64 bytes: options first, vc second
# 4. Load the issuer public key from embedded keys document
# Key path: keys_data["issuer"]["publicKeyJwk"]
issuer_jwk = keys_data["issuer"]["publicKeyJwk"]
x = base64url_decode(issuer_jwk["x"])
y = base64url_decode(issuer_jwk["y"])
pub_key = ec.EllipticCurvePublicNumbers(
int.from_bytes(x, "big"),
int.from_bytes(y, "big"),
ec.SECP256R1(),
).public_key()
# 5. Verify ECDSA P-256 signature
# sig_bytes is DER-encoded (from cryptography library's sign())
try:
pub_key.verify(sig_bytes, combined, ec.ECDSA(hashes.SHA256()))
return {"verified": True, "method": "offline_vc"}
except Exception:
return {"verified": False, "reason": "Signature invalid"}
# Usage
result = verify_offline("signed_contract.pdf")
print("Verified:", result["verified"])
if result["verified"]:
print("Document is cryptographically valid — no network needed.")
else:
print("Reason:", result.get("reason", "unknown"))Node.js (crypto)
// Offline verification of a SignForge-signed PDF
// Requires: npm install pdf-lib base-x
const { PDFDocument, PDFName, PDFDict, PDFArray, PDFHexString } = require("pdf-lib");
const crypto = require("crypto");
const fs = require("fs");
const base58 = require("base-x")(
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
);
// JCS canonicalization (RFC 8785): sorted keys, compact, no spaces
function jcsCanon(obj) {
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
if (Array.isArray(obj)) return "[" + obj.map(jcsCanon).join(",") + "]";
return "{" + Object.keys(obj).sort().map(
(k) => JSON.stringify(k) + ":" + jcsCanon(obj[k])
).join(",") + "}";
}
// Extract embedded file bytes from PDF by name
function extractEmbedded(pdfDoc, name) {
const namesDict = pdfDoc.catalog.lookup(PDFName.of("Names"));
if (!namesDict) return null;
const embFiles = namesDict.lookup(PDFName.of("EmbeddedFiles"));
if (!embFiles) return null;
const namesArr = embFiles.lookup(PDFName.of("Names"));
if (!namesArr) return null;
for (let i = 0; i < namesArr.size(); i += 2) {
const entry = namesArr.lookup(i);
if (entry?.toString?.()?.includes(name) || entry?.value === name) {
const fileSpec = namesArr.lookup(i + 1);
const ef = fileSpec.lookup(PDFName.of("EF"));
const stream = ef.lookup(PDFName.of("F"));
return Buffer.from(stream.getContents());
}
}
return null;
}
async function verifyOffline(pdfPath) {
const pdfBytes = fs.readFileSync(pdfPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
// 1. Extract embedded VC and keys
const vcBuf = extractEmbedded(pdfDoc, "signforge_receipt.vc.json");
const keysBuf = extractEmbedded(pdfDoc, "signforge_keys.json");
if (!vcBuf || !keysBuf) return { verified: false, reason: "No embedded proof" };
const vc = JSON.parse(vcBuf.toString("utf-8"));
const keys = JSON.parse(keysBuf.toString("utf-8"));
// 2. Decode signature (base58btc with 'z' multibase prefix)
const { proof, ...vcBody } = vc;
const sigBytes = base58.decode(proof.proofValue.slice(1));
// 3. Rebuild canonical hashes — options_hash FIRST, vc_hash SECOND
const { proofValue, ...proofOpts } = proof;
const vcHash = crypto.createHash("sha256")
.update(jcsCanon(vcBody), "utf-8").digest();
const optsHash = crypto.createHash("sha256")
.update(jcsCanon(proofOpts), "utf-8").digest();
const combined = Buffer.concat([optsHash, vcHash]);
// 4. Load issuer public key: keys.issuer.publicKeyJwk
const jwk = keys.issuer.publicKeyJwk;
const pubKey = crypto.createPublicKey({ key: jwk, format: "jwk" });
// 5. Verify ECDSA P-256 (sig is DER-encoded)
const valid = crypto.verify("sha256", combined, pubKey, sigBytes);
return { verified: valid, method: "offline_vc" };
}
verifyOffline("signed_contract.pdf").then(console.log);Transparency Log
SignForge maintains a public Merkle transparency log — the same cryptographic structure used by Certificate Transparency to secure the web's TLS certificates. Every signed document is appended to the log, and the tree head is independently signed with a separate key.
Get current tree state
curl https://signforge.io/api/transparency/latest{
"tree_size": 142,
"merkle_root": "7f8e9da1b2c3d4e5f6...",
"signed_tree_head": "eyJhbGciOiJFUzI1NiIs...",
"updated_at": "2026-04-14T08:30:00Z"
}Get public keys
Two separate keys protect the system. The issuer key signs Verifiable Credentials. The log key signs tree heads. Compromise of one does not break the other.
curl https://signforge.io/api/transparency/keys{
"version": 1,
"issuer": {
"did_key": "did:key:zDnae...",
"fingerprint": "sha256:f83OJ3D2...",
"algorithm": "ECDSA P-256",
"purpose": "Signs W3C Verifiable Credential signing receipts",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2...",
"y": "x_FEzRu9..."
}
},
"log": {
"did_key": "did:key:zDnae...",
"fingerprint": "sha256:kH7bEMqz...",
"algorithm": "ECDSA P-256",
"purpose": "Signs transparency log Signed Tree Heads",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "kH7bEMqz...",
"y": "pR2sTuVw..."
}
}
}DID:web document
The keys are also published as a W3C DID document at the well-known endpoint, enabling automated discovery by verifiers and wallets.
curl https://signforge.io/.well-known/did.jsonResponse Schemas
Verify Response
| Field | Type | Description |
|---|---|---|
| verified | boolean | Whether the document is verified |
| verification_code | string | The sf_ code for this document |
| envelope_title | string | Title of the signed envelope |
| sender | object | { name, email } of the sender |
| signer | object | { name, email } of the signer |
| signed_at | ISO 8601 | When the document was signed |
| original_sha256 | string | Hash of the original PDF |
| signed_sha256 | string | Hash of the signed PDF |
| method | enum | verifiable_credential | embedded_json | hash_lookup | cryptographic_signature |
| vc_valid | boolean | Whether the W3C VC signature is valid |
| transparency_log | object | null | Merkle proof and tree head if available |
Transparency Log Entry
| Field | Type | Description |
|---|---|---|
| entry_id | integer | Sequential log entry ID |
| document_hash | string | SHA-256 hash of the signed PDF |
| envelope_id | UUID | ID of the envelope |
| verification_code | string | The sf_ verification code |
| merkle_root | string | Merkle tree root hash at time of logging |
| merkle_proof | string[] | Proof path for independent verification |
| signed_tree_head | string | ECDSA-signed tree head (JWS) |
| created_at | ISO 8601 | When the entry was logged |
Rate Limits
| Endpoint | Limit |
|---|---|
| GET /api/verify/{code} | 30 req/min |
| POST /api/verify/upload | 10 req/min |
| GET /api/transparency/* | 30 req/min |
| GET /.well-known/did.json | 30 req/min |
Rate limits are per IP address. Exceeding the limit returns HTTP 429. No authentication is needed for these endpoints.
AI-Assisted Verification
Users can verify documents using AI tools. Upload a signed PDF to ChatGPT or Claude, and the AI can hash it, query the transparency log API, and confirm authenticity.
Example prompt for ChatGPT or Claude
“I have a signed PDF from SignForge. Please compute the SHA-256 hash and check it against the SignForge transparency log at https://signforge.io/api/transparency/hash/[HASH]. Then extract the embedded signforge_receipt.vc.json and verify the ECDSA signature.”
API Reference
Full REST API documentation with endpoint reference.
MCP Server
Use SignForge from Claude Desktop and AI agents.
Code Examples
Integration samples for n8n, Retool, cURL, and more.