Skip to main content

Signatures

A signature is a wallet-produced attestation that a specific file existed with a specific hash at a specific time. Filedgr verifies it, anchors it on-chain, and serves it back to anyone who asks.

Filedgr never signs for you

You build the statement, you sign it with your own wallet key, and you submit both. The API verifies the signature and rejects anything it cannot recover to a wallet you control.

That makes the exact signing procedure part of the contract, and it is not the usual one.

The signing procedure

danger

The statement is signed as a raw keccak256 hash, with no EIP-191 prefix. personal_sign, eth_sign, ethers signMessage and wagmi signMessage all prepend \x19Ethereum Signed Message:\n32 and will produce a signature that always fails verification. Sign the digest directly.

  1. Build the statement object.
  2. Serialise it to canonical JSON: keys sorted, no whitespace.
  3. Take the keccak256 of those bytes.
  4. Sign that digest directly with the wallet's private key.
import orjson
from eth_account import Account
from eth_utils import keccak

statement = {
"version": "1.0.0",
"purpose": "file-integrity-proof",
"nonce": "0x" + "ab" * 32,
"signed_at": "2026-08-26T12:00:00Z",
"file": {
"hash": {"algorithm": "SHA-256", "value": "0x" + "cd" * 32},
"cid": "bafy...",
"tx_hash": "0x" + "ef" * 32,
},
}

payload = orjson.dumps(statement, option=orjson.OPT_SORT_KEYS)
digest = keccak(payload)
signed = Account._sign_hash(digest, private_key=PRIVATE_KEY) # raw digest, no prefix

Submit the statement exactly as serialised — any difference in field values changes the digest.

danger

signed_at is re-serialised on the server before the digest is recomputed, so the platform does not hash the exact bytes you sent. A zero fraction is dropped (...00.000Z becomes ...00Z) and millisecond precision is rewritten (...00.123Z becomes ...00.123000Z), which means a statement signed with either form fails verification and is stored with status: ERROR.

Serialise signed_at as UTC with no fractional seconds (2026-08-26T12:00:00Z) or with exactly six digits (2026-08-26T12:00:00.123456Z), and sign that exact string.

Required literal values

Several fields are fixed strings. Anything else is rejected:

FieldRequired value
signature.algorithmECDSA_SECP256K1
signature.payload_formatcanonical-json-v1
statement.purposefile-integrity-proof
statement.file.hash.algorithmSHA-256

statement.version must be a semver string. nonce and the hash values must be 0x followed by 64 hex characters. The nonce exists to keep each signed payload distinct — the platform does not check it for reuse, so generate a fresh one per statement.

Submitting

curl -X POST "$FILEDGR_API/signatures" \
-H "x-api-key: $FILEDGR_API_KEY" -H "x-api-secret: $FILEDGR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"vault_id": "<uuid>",
"vault_network_id": "<on-chain vault id>",
"stream_id": "<uuid>",
"stream_network_id": "<on-chain stream id>",
"ledger": "POLYGON_POS",
"visibility": "PUBLIC",
"signatures": [ { "statement": { }, "signature": { } } ]
}'
note

visibility defaults to PRIVATE. Set it to PUBLIC if the signature should be readable through the Explorer without credentials.

Why a batch gets rejected

Three checks run before anything is stored, and each rejects the whole batch:

CheckStatus
Every file.hash must exist in the target stream, and the statement's cid must match the stored one400
The signing wallet must be permissioned for every referenced file403
The signing wallet must be one of your wallet credentials403

That last check is what stops one account attesting to another account's files.

warning

A 200 does not mean every signature succeeded. If the cryptographic verification itself fails, that item is stored with status: ERROR, is never anchored, and is returned inside a 200 response. Always inspect the per-item status in the response array rather than trusting the status code.

Completion is asynchronous

The POST returns once the batch is accepted. Each signature then advances through FILEDGR_RECEIVEDFILEDGR_REVIEWED / DCSTORAGE_UPLOADEDDLT_MINTEDFILEDGR_SIGNATURE_COMPLETED.

Subscribe to the signature.completed and signature.error webhook events rather than polling.

Reading signatures back

GET /signatures/{file_hash}/streams/{stream_network_id}

Note it is keyed by file hash and stream, not by signature id, and returns 204 when there are none.

Each row is enriched with signer_email on a best-effort basis by reverse-resolving the wallet. It is null for signers with no email credential, so do not rely on it for identification — the wallet address is the identity.

Public verification

Anyone can verify a file without credentials through the Explorer API — see Digital Twins.