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:

  1. 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:write and certificates:read. Keys look like imut_live_ followed by 32 characters and are shown once at creation.
  2. 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 is HMAC-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 via GET /api/v1/proofs/{proofId}?includeSalt=true or 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?

StatusCodeMeaning
400INVALID_HASHhash is not a 64-character hex SHA-256 digest
400INVALID_WORKSPACEworkspace is missing or not a valid id
401Missing or invalid API key
403API_ACCESS_DISABLEDThe organisation's plan does not include API access
403The key lacks the required scope
429Rate 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