I built ProofLedger to solve a simple problem: proving when a file existed matters, but EXIF data can be faked and file timestamps can be changed. Blockchain anchors can't be. Here's how to integrate the full workflow from local file to immutable proof using Python and the v1 REST API.

Hash First, Upload Never

The core insight is that you never upload files to get blockchain timestamps. You hash locally and anchor the SHA-256. The file stays on your machine. The hash goes on-chain.

import hashlib
import requests
import time
import json

def hash_file(file_path):
    """Generate SHA-256 hash of a file."""
    sha256_hash = hashlib.sha256()
    with open(file_path, "rb") as f:
        for byte_block in iter(lambda: f.read(4096), b""):
            sha256_hash.update(byte_block)
    return sha256_hash.hexdigest()

# Example usage
file_path = "important_document.pdf"
file_hash = hash_file(file_path)
print(f"SHA-256: {file_hash}")

This gives you a 64-character hex string that uniquely identifies your file. Same file, same hash. Different file, different hash. Change one byte, completely different hash.

Anchor the Hash via REST API

ProofLedger's v1 API has three endpoints. The first one submits your hash for anchoring to both Polygon and Bitcoin:

def anchor_hash(sha256_hash, filename=None, bitcoin_requested=True):
    """Submit hash for blockchain anchoring."""
    url = "https://proofledger.io/api/v1/proof"
    headers = {
        "Authorization": "Bearer sk_YOUR_KEY_HERE",
        "Content-Type": "application/json"
    }
    
    payload = {
        "sha256": sha256_hash,
        "bitcoin_requested": bitcoin_requested
    }
    
    if filename:
        payload["filename"] = filename
    
    response = requests.post(url, headers=headers, json=payload)
    
    if response.status_code == 201:
        return response.json()
    elif response.status_code == 429:
        print("Rate limited. Wait and retry.")
        return None
    elif response.status_code == 401:
        print("Invalid API key.")
        return None
    else:
        print(f"Error: {response.status_code} - {response.text}")
        return None

# Anchor our file hash
proof_response = anchor_hash(file_hash, "important_document.pdf")
if proof_response:
    proof_id = proof_response["id"]
    print(f"Proof ID: {proof_id}")
    print(f"Certificate URL: {proof_response['certificate_url']}")

You get back a proof ID and certificate URL immediately. But the anchoring happens asynchronously. Polygon is fast (seconds), Bitcoin takes longer (batched daily with merkle proofs).

Poll for Completion and Verify

The second endpoint lets you check anchoring status. The third endpoint is public verification that anyone can use:

def check_proof_status(proof_id):
    """Check anchoring status for a specific proof."""
    url = f"https://proofledger.io/api/v1/proof/{proof_id}"
    headers = {"Authorization": "Bearer sk_YOUR_KEY_HERE"}
    
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.json()
    else:
        print(f"Error checking status: {response.status_code}")
        return None

def verify_hash_publicly(sha256_hash):
    """Verify hash using public endpoint (no auth required)."""
    url = f"https://proofledger.io/api/v1/verify?hash={sha256_hash}"
    
    response = requests.get(url)
    if response.status_code == 200:
        return response.json()
    elif response.status_code == 429:
        print("Rate limited on verification (120/hr per IP)")
        return None
    else:
        print(f"Verification error: {response.status_code}")
        return None

# Poll until anchored
def wait_for_anchoring(proof_id, timeout=300):
    """Wait for proof to be anchored on Polygon."""
    start_time = time.time()
    
    while time.time() - start_time < timeout:
        status = check_proof_status(proof_id)
        if not status:
            break
            
        polygon_status = status.get("polygon_status")
        print(f"Polygon status: {polygon_status}")
        
        if polygon_status == "anchored":
            print(f"Anchored! Tx: {status['chain_tx']}")
            print(f"Explorer: {status['explorer_url']}")
            return status
        elif polygon_status == "failed":
            print("Anchoring failed")
            return None
            
        time.sleep(10)  # Check every 10 seconds
    
    print("Timeout waiting for anchoring")
    return None

# Wait for our proof to be anchored
final_status = wait_for_anchoring(proof_id)

# Verify using public endpoint (third-party verification)
verification = verify_hash_publicly(file_hash)
if verification and verification.get("found"):
    proof_data = verification["proof"]
    print(f"Publicly verified! Anchored at: {proof_data['created_at']}")
    print(f"Blockchain: {proof_data['blockchain']}")
    print(f"Transaction: {proof_data['chain_tx']}")

The public verification endpoint is the key. Anyone can check if a hash was anchored without needing your API key. That's what makes it useful for disputes or third-party verification.

Handle the Edge Cases

Real integration means handling failures gracefully:

def robust_file_anchor(file_path, api_key, max_retries=3):
    """Complete workflow with error handling."""
    
    # Step 1: Hash the file
    try:
        file_hash = hash_file(file_path)
    except FileNotFoundError:
        print(f"File not found: {file_path}")
        return None
    except Exception as e:
        print(f"Hashing error: {e}")
        return None
    
    # Step 2: Submit for anchoring with retries
    for attempt in range(max_retries):
        proof_response = anchor_hash(file_hash, file_path.split('/')[-1])
        
        if proof_response:
            break
        elif attempt < max_retries - 1:
            print(f"Retry {attempt + 1}/{max_retries} in 30 seconds...")
            time.sleep(30)
        else:
            print("Failed to submit after all retries")
            return None
    
    # Step 3: Wait for anchoring
    proof_id = proof_response["id"]
    final_status = wait_for_anchoring(proof_id)
    
    if not final_status:
        print("Anchoring incomplete or failed")
        return None
    
    # Step 4: Return verification data
    return {
        "file_hash": file_hash,
        "proof_id": proof_id,
        "blockchain_tx": final_status["chain_tx"],
        "explorer_url": final_status["explorer_url"],
        "certificate_url": proof_response["certificate_url"],
        "verification_url": f"https://proofledger.io/verify.html?hash={file_hash}"
    }

# Usage
result = robust_file_anchor("important_document.pdf", "sk_YOUR_KEY_HERE")
if result:
    print(f"Success! Hash {result['file_hash']} anchored.")
    print(f"Verification URL: {result['verification_url']}")

What This Gets You

The result is a permanent, immutable timestamp that proves your file existed at a specific point in time. The hash is anchored on both Polygon (fast) and Bitcoin (daily batches with merkle proofs). Anyone can verify it independently using the public endpoint.

Your file never leaves your machine. Only the SHA-256 goes on-chain. That means this works for any file size, any file type, any sensitivity level.

I built this API after seeing too many disputes where documentation existed but couldn't be verified. Having a photo isn't the same as proving when it was taken. The blockchain makes that distinction permanent and independently verifiable.

The verify-proof PyPI package also does offline verification if you want to avoid API calls entirely. But the REST API gives you the full workflow from hash to proof to verification in whatever language you prefer.