Back to API Docs
Public API — No Authentication Required

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.

Layer 1

W3C VC

Validate the embedded Verifiable Credential with ECDSA P-256

Layer 2

DB Lookup

Match verification code against the signing database

Layer 3

Hash Lookup

Match document SHA-256 against the transparency log

Layer 4

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

GET
/api/verify/{code}
POST
/api/verify/upload

Transparency Log

GET
/api/transparency/latest
GET
/api/transparency/hash/{sha256}
GET
/api/transparency/entry/{id}
GET
/api/transparency/keys

DID Discovery

GET
/.well-known/did.json24h cache

Verify 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.

bash
curl https://signforge.io/api/verify/sf_abc123def456

Response

json
{
  "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

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

javascript
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

bash
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.

bash
# 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/$HASH

Response

json
{
  "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)

python
"""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)

javascript
// 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

bash
curl https://signforge.io/api/transparency/latest
json
{
  "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.

bash
curl https://signforge.io/api/transparency/keys
json
{
  "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.

bash
curl https://signforge.io/.well-known/did.json

Response Schemas

Verify Response

FieldTypeDescription
verifiedbooleanWhether the document is verified
verification_codestringThe sf_ code for this document
envelope_titlestringTitle of the signed envelope
senderobject{ name, email } of the sender
signerobject{ name, email } of the signer
signed_atISO 8601When the document was signed
original_sha256stringHash of the original PDF
signed_sha256stringHash of the signed PDF
methodenumverifiable_credential | embedded_json | hash_lookup | cryptographic_signature
vc_validbooleanWhether the W3C VC signature is valid
transparency_logobject | nullMerkle proof and tree head if available

Transparency Log Entry

FieldTypeDescription
entry_idintegerSequential log entry ID
document_hashstringSHA-256 hash of the signed PDF
envelope_idUUIDID of the envelope
verification_codestringThe sf_ verification code
merkle_rootstringMerkle tree root hash at time of logging
merkle_proofstring[]Proof path for independent verification
signed_tree_headstringECDSA-signed tree head (JWS)
created_atISO 8601When the entry was logged

Rate Limits

EndpointLimit
GET /api/verify/{code}30 req/min
POST /api/verify/upload10 req/min
GET /api/transparency/*30 req/min
GET /.well-known/did.json30 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.