Quickstart: create a blockchain proof from a file hash
immut records a commitment to your file's SHA-256 hash on the XRP Ledger. The file never leaves your machine: you hash it locally and send only the 64-character hex digest. This page takes you from nothing to a verified proof using curl.
What do I need before I start?
Two things, both one-time setup:
- An API key. A human account owner creates one at app.immut.io/account?tab=api-keys (Professional and Enterprise plans include API access). For agents, request only the scopes
documents:writeandcertificates:read. Keys look likeimut_live_followed by 32 characters and are shown once at creation. - A workspace id. Every proof belongs to a workspace. List yours once and save the id:
export IMMUT_API_KEY="imut_live_..."
curl -s https://backend.immut.io/api/v1/workspaces \
-H "Authorization: Bearer $IMMUT_API_KEY"
{ "success": true, "data": [ { "_id": "6a343b4d0de29d485cd1f710", "name": "Default Workspace" } ] }
Save the _id of the workspace you want proofs to live in:
export IMMUT_WORKSPACE_ID="6a343b4d0de29d485cd1f710"
Step 1: how do I hash the file locally?
Any SHA-256 implementation works. The digest must be 64 hex characters.
# macOS / Linux
shasum -a 256 report-final.pdf | cut -d' ' -f1
// Node.js (streams, works for large files)
const crypto = require('crypto');
const fs = require('fs');
const hash = crypto.createHash('sha256');
fs.createReadStream('report-final.pdf')
.on('data', (d) => hash.update(d))
.on('end', () => console.log(hash.digest('hex')));
# Python
import hashlib
print(hashlib.sha256(open("report-final.pdf", "rb").read()).hexdigest())
Step 2: how do I create the proof?
POST the hash and workspace id. fileName, fileSize, mimeType, and metadata.description are optional but make certificates and listings more useful.
curl -s -X POST https://backend.immut.io/api/v1/proofs \
-H "Authorization: Bearer $IMMUT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"hash": "d28771e1bf6a7aa31693a4c81203b9cb1d51f9d1c20c1d7ad6db4035f4eaff6b",
"workspace": "'$IMMUT_WORKSPACE_ID'",
"fileName": "report-final.pdf",
"fileSize": 48213,
"mimeType": "application/pdf",
"metadata": { "description": "Q2 risk assessment, finalised version" }
}'
A successful response returns HTTP 201:
{
"success": true,
"data": {
"proofId": "6a55f4b3f8878465844f8b43",
"txHash": "02790DA05EF2999C77A2B462CE0F7A1E11A52FB295F3AE3FF542112B12B7A8E5",
"verifyUrl": "https://livenet.xrpl.org/transactions/02790DA05EF2999C77A2B462CE0F7A1E11A52FB295F3AE3FF542112B12B7A8E5/detailed",
"certPath": "/api/v1/certificates/6a55f4b3f8878465844f8b43",
"ledger": "mainnet",
"ledgerIndex": 105588998,
"timestamp": "2026-07-14T08:34:59.412Z",
"hashScheme": "hmac-sha256-nonce-v3",
"proofCommitment": "9f2c41d8a0b3e5f6c7d8e9fa0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c",
"proofNonce": "4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f"
}
}
Step 3: what must I store?
Write the response next to the file, for example as a report-final.pdf.immut.json sidecar. Three fields matter forever:
txHash: the permanent anchor on the XRP Ledger.proofNonce: required to verify a salted proof. Under the default scheme the value on the public ledger isHMAC-SHA-256(proofNonce, sha256(file)), not the raw file hash, so verification without the nonce is impossible. If you lose your local copy, recover it viaGET /api/v1/proofs/{proofId}?includeSalt=trueor from the certificate PDF, which embeds it.proofId: needed for status polling and certificate download.
Anchoring is synchronous: a 201 response means the transaction is already validated on the ledger and blockchainStatus is completed. Polling GET /api/v1/proofs/{proofId} exists for the rare case where a response was lost mid-flight or status shows processing.
Download the court-ready certificate PDF any time (scope certificates:read):
curl -s https://backend.immut.io/api/v1/certificates/$PROOF_ID \
-H "Authorization: Bearer $IMMUT_API_KEY" -o certificate.pdf
Step 4: how does anyone verify the proof later?
Verification is public and needs no API key:
curl -s https://backend.immut.io/api/public/verify/02790DA05EF2999C77A2B462CE0F7A1E11A52FB295F3AE3FF542112B12B7A8E5
{
"success": true,
"data": {
"transactionHash": "02790DA05EF2999C77A2B462CE0F7A1E11A52FB295F3AE3FF542112B12B7A8E5",
"network": "mainnet",
"verified": true,
"ledgerIndex": 105588998,
"ledgerCloseTime": "2026-07-14T08:35:02.000Z",
"memo": { "fileHash": "...", "hashScheme": "hmac-sha256-nonce-v3" }
}
}
Then confirm the file matches. For the default salted scheme: recompute sha256(file), compute HMAC-SHA-256 with the stored proofNonce as key over the raw digest bytes, and compare the result with memo.fileHash. For plain-scheme proofs (sha256-plain-v1), compare sha256(file) with memo.fileHash directly. Non-technical verifiers can use the web page at app.immut.io/verify, which does this in the browser.
What happens if I prove the same file twice?
The endpoint is idempotent per hash and workspace. A second POST with the same hash returns HTTP 200 with "alreadyProven": true and the original proof data, including the stored nonce. Nothing is written twice and nothing extra is charged.
What errors should my integration handle?
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_HASH | hash is not a 64-character hex SHA-256 digest |
| 400 | INVALID_WORKSPACE | workspace is missing or not a valid id |
| 401 | Missing or invalid API key | |
| 403 | API_ACCESS_DISABLED | The organisation's plan does not include API access |
| 403 | The key lacks the required scope | |
| 429 | Rate limit exceeded. Respect the Retry-After header and back off |
Rate limits are 60 requests per minute and 10,000 per day per key.
Where do I go next?
- API reference for every endpoint including status polling and certificates
- Agent playbook if an AI agent will run this flow autonomously
- Security for scopes, the commitment scheme, and data handling rules