API Reference
The ProofLedger API lets integrators submit SHA-256 hashes for anchoring on the Polygon blockchain, retrieve proof status, and look proofs up. Checking a proof without ProofLedger uses the bundle and the open-source verify-proof tool (section 8). Authenticated endpoints are available on every plan, including Free; a public, no-auth verify endpoint lets anyone look a hash up.
01 Overview
All requests are made against https://proofledger.io. All responses are JSON, UTF-8 encoded. The API is versioned under /api/v1/; breaking changes will ship under a new version prefix.
Clients: the verify-proof package carries a ProofLedger API client for Python (pip install verify-proof) and Node (npm install verify-proof). A Postman collection covers every endpoint below with one variable for your key, and the OpenAPI 3.1 spec generates clients in other languages. No code at all: the n8n community node (n8n-nodes-proofledger) anchors a file from any n8n workflow and its trigger starts one when a proof is anchored, signature checked. The same on Zapier: accept the invite (the integration is private until Zapier lists it publicly) and you get Create Proof From Hash / From File, Find Proof, Verify Hash and a Proof Anchored trigger. And on Make: accept the invite for Create a proof, Get a proof (by ID or external ID), Get a proof bundle, Verify a hash, List proofs, Make an API call and a Watch anchored proofs trigger. In Claude Code, the proofledger plugin gives a coding agent the same tools: /plugin marketplace add Fulcrum-Enterprises/verify-proof, then /plugin install proofledger@proofledger. In Cursor, one click: add the verify-proof MCP server to Cursor (it starts with uvx, so uv is the only requirement). For your own pages, embeds: a certificate card, a status badge and a verify widget, one tag each.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /api/v1/proof |
Submit a SHA-256 hash for anchoring | API key (any plan) |
| POST | /api/v1/proofs/batch |
Submit up to 100 hashes in one call | API key (any plan) |
| GET | /api/v1/proof/:id |
Retrieve proof status and anchor details | API key (any plan) |
| GET | /api/v1/proofs |
Look up by your external_id, or list proofs anchored since a time |
API key (any plan) |
| POST | /api/v1/webhooks |
Register an https URL to be told when a proof is anchored (also GET, DELETE, /:id/test) |
API key (any plan) |
| GET | /api/v1/verify |
Verify any proof by SHA-256 hash | Public |
The API is intended for software that records proofs on its users' behalf: music collaboration and split-sheet tools, beat marketplaces, document and e-signature apps, photo and evidence apps. For occasional single-file work, the web dashboard is usually simpler. The platforms page has a five-minute quickstart; every change to this API is on the changelog.
02 Authentication & Access
Proof submission and retrieval require an API key. Keys are issued from the Account page on every plan, including Free, with no credit card required to make your first call. Creating a key or calling the API accepts the API Terms; the providers that handle API data are listed on the subprocessors page.
Monthly API proof allowances by plan: Free 100, Developer 1,000, Platform 10,000, counted per calendar month (UTC). On Free, requests past 100 are refused with 429 until the 1st. On Developer and Platform, proofs past the allowance keep working and cost $0.01 each, billed with the subscription. See the pricing page.
Upgrade at Pricing, or email sales@proofledger.io for more than 10,000 proofs a month.
- Sign in to your account.
- Scroll to the API Keys section.
- Enter a label (e.g. "Production") and click Create Key.
- Copy the key immediately: it is shown once and cannot be retrieved later.
Keys look like sk_ab12cd34_.... Store them as you would any secret: environment variable, secrets manager, or vault. Never commit keys to source control or send them in query strings.
Pass the key as a Bearer token in the Authorization header:
Authorization: Bearer sk_ab12cd34_yourLongSecretHere
Keys can be revoked at any time from the Account page. A revoked key is refused from the next request on. If a key is compromised, revoke it immediately and issue a new one; past proofs remain valid regardless.
03 Submit a Proof
| Field | Type | Required | Description |
|---|---|---|---|
sha256 | string | yes | 64-character SHA-256 hex digest (lowercase or uppercase). |
filename | string | no | Optional label (≤500 chars). Shown on the public certificate, so keep personal details out of it. |
external_id | string | no | Your own record id for this file (≤128 chars, private, unique per account). Send the same request twice and you get the same proof back with 200 and already_existed: true, so retries are safe. |
# 1. Hash the file locally; never send the file itself. sha256sum site-inspection.pdf # a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 # 2. Submit the hash. curl -X POST https://proofledger.io/api/v1/proof \ -H "Authorization: Bearer sk_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "filename": "site-inspection.pdf" }'
{
"id": 42,
"sha256": "a1b2c3d4...f0a1b2",
"filename": "site-inspection.pdf",
"external_id": null,
"status": "approved",
"polygon_status": "PENDING",
"created_at": "2026-04-21T14:22:03.000Z",
"polygon_anchored_at": null,
"issued_via": null,
"chain_tx": null,
"certificate_url": "https://proofledger.io/cert/42",
"verification_url": "https://proofledger.io/verify.html?hash=a1b2c3d4...f0a1b2",
"explorer_url": null,
"duplicate_of": null,
"already_existed": false
}
The proof is accepted and queued for on-chain anchoring. Poll GET /api/v1/proof/:id or call the public verify endpoint to see the Polygon tx hash (typically under a minute).
duplicate_of pointer to the earliest known record. To make your own retries safe, send an external_id: the same id on the same account always returns the proof it first created. For a request that timed out before you saw the answer, there is also the Idempotency-Key header (any unique string, up to 200 characters): a resend with the same key and the same body within 24 hours returns the stored response, same status and JSON, with Idempotent-Replayed: true, and nothing runs twice. The same key with a different body is refused with 422; only successful responses are stored, so after a 400 the same key accepts a corrected body. The header works on this endpoint, on the batch endpoint and on webhook registration, where a replay returns the same endpoint and secret instead of creating a second one.curl -X POST https://proofledger.io/api/v1/proofs/batch \ -H "Authorization: Bearer sk_YOUR_KEY_HERE" \ -H "Content-Type: application/json" \ -d '{ "items": [ {"sha256": "a1b2c3d4...f0a1b2", "external_id": "beat-1001", "filename": "beat-1001.wav"}, {"sha256": "f0e1d2c3...1e0f", "external_id": "beat-1002", "filename": "beat-1002.wav"} ] }'
{
"batch_id": "api-batch-1760400000000",
"summary": { "received": 2, "created": 1, "exists": 1, "invalid": 0 },
"monthly_limit": 10000, "used_this_month": 118, "remaining": 9882, "overage": 0,
"items": [
{ "index": 0, "result": "created", "proof": { "id": 431, "external_id": "beat-1001", "polygon_status": "PENDING", ... } },
{ "index": 1, "result": "exists", "proof": { "id": 388, "external_id": "beat-1002", "polygon_status": "ANCHORED", ... } }
]
}
Each item's result is created, exists (that external_id was already recorded; the existing proof is returned and nothing is charged) or invalid (with an error). An item without an external_id is always created.
# Everything anchored since the last check, 100 at a time curl "https://proofledger.io/api/v1/proofs?anchored_since=2026-10-14T00:00:00Z&limit=100" \ -H "Authorization: Bearer sk_YOUR_KEY_HERE" # -> { "proofs": [ ... ], "next_cursor": 388 } (null when there is no further page)
04 Retrieve a Proof
curl https://proofledger.io/api/v1/proof/42 \
-H "Authorization: Bearer sk_YOUR_KEY_HERE"
{
"id": 42,
"sha256": "a1b2c3d4...f0a1b2",
"filename": "site-inspection.pdf",
"status": "approved",
"polygon_status": "ANCHORED",
"bitcoin_status": "NOT_REQUESTED",
"evidence_readiness": "READY_POLYGON",
"created_at": "2026-04-21T14:22:03.000Z",
"chain_tx": "0xabc123...",
"chain_name": "polygon-mainnet",
"bitcoin_txid": null,
"btc_payment_status": "NONE",
"certificate_url": "https://proofledger.io/cert/42",
"verification_url": "https://proofledger.io/verify.html?hash=a1b2c3d4...f0a1b2",
"explorer_url": "https://polygonscan.com/tx/0xabc123...",
"bitcoin_explorer_url": null
}
| Field | Possible values |
|---|---|
status | approved, pending, pending_review, denied, contested |
polygon_status | ANCHORED · PENDING |
bitcoin_status | NOT_REQUESTED for every proof created since 2026-10-15; ANCHORED only on proofs anchored on Bitcoin before then (legacy field, kept so responses do not change shape) |
evidence_readiness | READY_POLYGON · PENDING_POLYGON · NEEDS_REVIEW · DENIED (READY_BITCOIN on legacy proofs) |
btc_payment_status | NONE (legacy field) |
Immediately after submission, polygon_status is PENDING and chain_tx is null. Anchoring starts as soon as the proof is accepted and usually completes within a minute. If the signing service is briefly unreachable the proof stays PENDING and is retried automatically.
4b Webhooks
Instead of polling, register an https URL and ProofLedger will POST a signed event to it when a proof's anchor lands: proof.polygon.anchored, usually within a minute of submission. (proof.bitcoin.anchored remains a valid event name for compatibility but no longer fires: Bitcoin anchoring was discontinued on 2026-10-15.) Events are sent for proofs created after the endpoint was registered; use GET /api/v1/proofs?anchored_since= to catch up on anything earlier or anything missed.
POST /your/path
ProofLedger-Signature: t=1760400123,v1=5f1c...9a07
ProofLedger-Event-Id: evt_8812
Content-Type: application/json
{
"id": "evt_8812",
"type": "proof.polygon.anchored",
"created_at": "2026-10-14T14:22:03.000Z",
"data": { "proof": { "id": 431, "external_id": "beat-1001", "sha256": "a1b2...f0a1b2",
"polygon_status": "ANCHORED", "chain_tx": "0x3b5b...", "polygon_anchored_at": "2026-10-14T14:22:01.115Z",
"certificate_url": "https://proofledger.io/cert/431", ... },
"bundle": { "format": "verify-proof/1", "hash": "a1b2...f0a1b2", "blockchain": "polygon",
"tx_id": "0x3b5b...", "anchored_at": "2026-10-14T14:22:01.115Z", ... } }
}
The header is t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 over the string t + "." + raw_body with your endpoint's secret. Recompute it over the raw bytes you received (not a re-serialised copy), compare in constant time, and reject timestamps more than five minutes old.
# Node const crypto = require('crypto'); function verify(secret, header, rawBody) { const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || ''); if (!m || Math.abs(Date.now() / 1000 - +m[1]) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest(); return crypto.timingSafeEqual(expected, Buffer.from(m[2], 'hex')); } # Python import hmac, hashlib, re, time def verify(secret, header, raw_body: bytes): m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "") if not m or abs(time.time() - int(m[1])) > 300: return False expected = hmac.new(secret.encode(), f"{m[1]}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, m[2])
- Answer with any 2xx within 10 seconds. Anything else, or no answer, counts as a failure.
- Failures are retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours (8 attempts over almost two days). If every attempt fails, the endpoint is switched off and the account's email address is told. Register it again when it is reachable (it gets a new secret) and catch up with
anchored_since. - Delivery is at least once: a slow 2xx can be retried, so treat the event
idas the key and ignore repeats. - Redirects are not followed, and response bodies are ignored.
4c Partner branding
On Developer and Platform, if your product records proofs for its own users, your product name can appear on the certificate and the public verify page as:
Recorded through <Your product> · Anchored by ProofLedger
Branding is approved by hand. Email support@proofledger.io with the product name and website; it is checked against impersonation before it is switched on. Once approved, every proof a Developer or Platform account creates afterwards carries the name. The name is copied onto each proof when it is created, so a certificate never changes later, and branded proofs do not show your account's email on public pages. The wording is fixed: it says who recorded the proof and who anchored it, nothing more.
{
"approved": true,
"display_name": "YourApp",
"website": "https://example.com",
"approved_at": "2026-10-14T12:00:00.000Z",
"line": "Recorded through YourApp · Anchored by ProofLedger"
}
Proof objects carry the same value as issued_via (null when the proof was not branded), including in webhook payloads and GET /api/v1/verify.
4d Proof bundles If ProofLedger goes away
A bundle is one JSON document with everything needed to verify a proof without ProofLedger: the hash, the chain, the transaction id and the anchor time. Store it next to the file. Its top-level fields are the proof-file format the open-source verify-proof package reads (section 8), so verify-proof verify <file> --proof bundle.json works offline, years later.
{
"format": "verify-proof/1",
"service": "proofledger",
"hash": "a1b2c3...",
"algorithm": "sha256",
"blockchain": "polygon",
"tx_id": "0xabc123...",
"anchored_at": "2026-10-15T02:11:09.512Z",
"merkle_path": [],
"anchored": true,
"proof_id": 42,
"external_id": "song-42",
"certificate_url": "https://proofledger.io/cert/42",
"anchors": {
"polygon": { "blockchain": "polygon", "anchored": true, "tx_id": "0xabc123...", "anchored_at": "...", "explorer_url": "https://polygonscan.com/tx/0xabc123..." },
"bitcoin": { "blockchain": "bitcoin", "anchored": false, "status": "NOT_REQUESTED", "tx_id": null, "merkle_path": null }
},
"how_to_verify": [ "..." ]
}
Before the Polygon anchor lands, anchored is false and tx_id is null; the bundle is worth storing once the proof.polygon.anchored webhook arrives, and that event's payload already carries it under data.bundle. The anchors.bitcoin block is present for shape compatibility and reads anchored: false on every proof created since 2026-10-15.
05 Verify a Proof Public
curl "https://proofledger.io/api/v1/verify?hash=a1b2c3d4...f0a1b2"
{
"found": true,
"proof": {
"id": 42,
"sha256": "a1b2c3d4...f0a1b2",
"filename": "site-inspection.pdf",
"created_at": "2026-04-21T14:22:03.000Z",
"polygon_anchored_at": "2026-04-21T14:22:41.000Z",
"polygon_status": "ANCHORED",
"bitcoin_status": "NOT_REQUESTED",
"evidence_readiness": "READY_POLYGON",
"chain_tx": "0xabc123...",
"bitcoin_txid": null,
"user_email": null
},
"certificate_url": "https://proofledger.io/cert/42",
"explorer_url": "https://polygonscan.com/tx/0xabc123...",
"bitcoin_explorer_url": null
}
{ "found": false, "sha256": "a1b2c3d4...f0a1b2" }
user_email stays in the response, always null, so existing code keeps working.06 Rate Limits & Quotas
| Endpoint | Scope | Limit | Window |
|---|---|---|---|
| API proofs, single and batch | Per account (Free) | 100, then refused | Calendar month (UTC) |
| API proofs, single and batch | Per account (Developer) | 1,000, then $0.01 each | Calendar month (UTC) |
| API proofs, single and batch | Per account (Platform) | 10,000, then $0.01 each | Calendar month (UTC) |
| Every call with an API key | Per key | 120 | One minute (fixed window) |
GET /api/v1/verify without a key | Per IP | 120 | One hour |
GET /api/v1/verify with a key | Per key | 120 | One minute (the key's limit, not the IP's) |
Every response to a keyed call carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). Over the limit you get 429 with a Retry-After header in seconds and {"error": "...", "retry_after": 12}; wait that long and retry. A refused call does not count as usage. The batch endpoint is one call for up to 100 hashes, so a partner that batches rarely meets this limit.
A batch answer carries monthly_limit, used_this_month, remaining and overage (how many of its proofs were past the allowance). For more than 10,000 a month, email sales@proofledger.io with your expected volume.
07 Error Codes
All errors return a JSON body of the form {"error": "..."}. Use the HTTP status code for programmatic decisions.
| Status | Meaning | Typical cause |
|---|---|---|
400 | Bad Request | Missing or malformed sha256; invalid proof ID. |
401 | Unauthorized | API key missing, malformed, or does not match a known key. |
403 | Forbidden | The account that owns the key is disabled. (Every plan, including Free, has API access.) |
404 | Not Found | Proof ID does not exist or is not owned by the authenticated account. |
409 | Conflict | A request with the same Idempotency-Key is still running. Retry in a moment. |
422 | Unprocessable | The same Idempotency-Key was already used with a different request body. |
429 | Too Many Requests | Free's monthly allowance used up, more than 120 requests a minute on one key, or more than 120 public verify checks an hour from one IP. |
500 | Server Error | Transient server fault. Retry with exponential backoff. |
08 Independent Verification
Every anchored ProofLedger proof can be checked without contacting ProofLedger's servers: the verify-proof Python package checks the file against its bundle offline, and the transaction can be looked up on any public block explorer.
pip install verify-proof # Hash a file: verify-proof hash ./evidence.pdf # Verify against a proof JSON downloaded from ProofLedger: verify-proof verify ./evidence.pdf --proof ./proof.json # Anchor a new file (needs a free API key in PROOFLEDGER_API_KEY): verify-proof create ./evidence.pdf
This is the path for anyone who needs to check a proof without trusting us: run verify-proof on your own machine, then confirm the anchoring transaction on a public block explorer, whether or not ProofLedger is online. Source: github.com/Fulcrum-Enterprises/verify-proof.
8b Embeds
Show a proof where the file lives. One tag puts a widget on any page. The widget is an <iframe> served by proofledger.io, so your page carries nothing but the loader, and the widgets load nothing from third parties. Add data-theme="dark" for dark pages. Style the frame through its class, .proofledger-embed (width and margins; the height follows the content).
<script async src="https://proofledger.io/embed.js" data-proof="84"></script>
File name, "Anchored on Polygon" with the recorded and anchored dates (or "Anchoring pending"), the shortened SHA-256 with the full hash on hover, your Recorded through line once approved, and a link to the full certificate. The proof ID is the id every API response returns.
<script async src="https://proofledger.io/embed.js" data-verify></script>
A drop zone. The visitor's browser hashes the file with SHA-256 and sends only the hash to the public verify endpoint; the file never leaves their device. The result says anchored, pending or no proof, and links to the certificate. The visitor's network gets that endpoint's limit, 120 checks an hour.
<img src="https://proofledger.io/embed/badge/84.svg" alt="ProofLedger proof status">
An SVG for READMEs, docs and emails, no script: "anchored on Polygon", "anchoring pending" or "no certificate". In Markdown: .
Only these pages allow framing (Content-Security-Policy: frame-ancestors *): they show public data, carry no session and take no action a click could be tricked into. Every other page on proofledger.io keeps X-Frame-Options: DENY. A missing or denied proof shows a "No certificate" card and never the reason. The card shows ProofLedger's view of the public record; it does not say your site verified anything.
8c Agent run receipts
Prove what an AI agent did, without storing what it saw. A receipt is a hash chain over the steps of one agent run: each step keeps only a SHA-256 fingerprint of its content (salted with a per-run secret), its type (prompt, tool_call, tool_result, output, note), its time and an optional short label. The chain head is anchored through POST /api/v1/proof as one ordinary proof, with external_id set to the run id, and the bundle is saved into the receipt. No new endpoint: the server only ever sees one hash per run.
The format, proofledger-agent-run-receipt/1, and the chain rule are documented in the open-source verify-proof package, which builds, anchors and checks receipts in Python and Node, from the command line, and through its MCP tools (start_run_receipt, add_receipt_step, anchor_run_receipt, verify_run_receipt), so an agent in Claude Code, Claude Desktop or Cursor can receipt its own work.
# Python from agent_receipt import RunReceipt r = RunReceipt("support-ticket-48213", agent={"name": "support-bot"}) r.prompt(customer_message, label="customer message") r.tool_call(json.dumps(call), label="lookup_order") r.tool_result(result_text) r.output(reply, label="reply sent") r.anchor() # one API proof; waits for the Polygon anchor r.save("ticket-48213.receipt.json") # Anyone, offline, without ProofLedger: verify-proof receipt verify ticket-48213.receipt.json
Checking a receipt recomputes the chain from the steps, compares it with the head, then checks the head against the bundle exactly as verify-proof verify checks a file; the transaction is confirmed on a block explorer. Given a step's content and the run secret, the checker can also confirm that step was made from that exact content. Remove the secret field before handing a receipt to a third party: the chain still verifies, and nothing in the file can be guessed.
A receipt shows that this exact sequence of steps existed by the anchor time and has not changed since. It does not show that the agent was right, or who ran it. Each receipt is one API proof on the account's plan. Package and examples: github.com/Fulcrum-Enterprises/verify-proof.
Ready to integrate?
Every plan includes API access. Free gives you 100 API proofs a month, enough to wire up an integration and demo it. Developer ($19 a month) includes 1,000 and Platform ($79 a month) 10,000 with team accounts and audit exports; past the allowance, each proof costs $0.01.
8d GitHub App
Install the ProofLedger GitHub App on a repository and every release you publish there is anchored on its own. GitHub reports each release file's SHA-256, so the App records one proof per file without downloading anything, writes a "Proof of existence" table into the release notes (file, hash, certificate link), and once the hash is on Polygon attaches <file>.proof.json, the bundle from section 4d, to the release and adds the transaction link. A second check fifteen minutes after publishing catches files uploaded after the release went out. A release with no files gets one proof of the release record, the SHA-256 of the text github-release/1\n<owner/repo>\n<tag>\n<commit>\n, with that recipe written under the table.
Setup: install the App, pick the repositories, and GitHub sends you to proofledger.io/github/connect to connect the installation to your ProofLedger account (sign in with GitHub, then with ProofLedger). Proofs count against that account's monthly allowance, the same as API proofs; when it is used up the notes say so once and nothing is recorded. The App asks GitHub for Contents read and write (to read the release, edit its notes and attach the bundle) and nothing else. Disconnect under Account → API Access; uninstall on GitHub. The GitHub Action stays for workflows that build the artifact themselves and want the proof in the same job.
09 Support
For API questions or integration help, contact:
ProofLedger LLC
support@proofledger.io
Related: FAQ & Support · For platforms · Changelog · Enterprise