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
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.
- Build the statement object.
- Serialise it to canonical JSON: keys sorted, no whitespace.
- Take the keccak256 of those bytes.
- 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.
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:
| Field | Required value |
|---|---|
signature.algorithm | ECDSA_SECP256K1 |
signature.payload_format | canonical-json-v1 |
statement.purpose | file-integrity-proof |
statement.file.hash.algorithm | SHA-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": { } } ]
}'
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:
| Check | Status |
|---|---|
Every file.hash must exist in the target stream, and the statement's cid must match the stored one | 400 |
| The signing wallet must be permissioned for every referenced file | 403 |
| The signing wallet must be one of your wallet credentials | 403 |
That last check is what stops one account attesting to another account's files.
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_RECEIVED → FILEDGR_REVIEWED / DCSTORAGE_UPLOADED → DLT_MINTED →
FILEDGR_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.