Signatures
A signature binds a wallet to a specific file: the signer states, in a structured payload, that a named file with a given hash is the one they mean — and signs that statement. Filedgr anchors it, so anyone can later re-check the file against the signature without trusting either party.
How signing works
- Build the statement — the file's hash and CID, the anchoring transaction, a nonce and a timestamp.
- Serialise it as canonical JSON with sorted keys.
- Take the keccak-256 digest of those bytes.
- Sign that digest directly with the wallet's private key — no EIP-191 prefix.
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
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 or with exactly six digits, and sign
that exact string.
Fixed values
Several fields are literals. 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 semver. nonce and the hash values are 0x plus 64 hex characters.
Generate a fresh nonce per statement.
Submitting
curl -X POST "https://api.filedgr.network/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": { } } ]
}'
Creating a signature costs 1 credit.
visibility defaults to PRIVATE. Set it to PUBLIC if the signature should be readable
through the Explorer without credentials.
A 200 does not mean every signature succeeded. Entries are stored individually, and a failure is
recorded as status: ERROR on that entry rather than as an HTTP error. Read the response.
Reading signatures back
On the partner API, signatures are looked up by file hash within a stream:
GET /signatures/{file_hash}/streams/{stream_network_id}
An empty result is 204 with no body, not an empty list.
Publicly — for a signature with visibility: PUBLIC — the Explorer API serves them with
no credentials at all:
GET https://explorer-api.filedgr.network/signatures/by-hash/{file_hash}
GET https://explorer-api.filedgr.network/signatures/by-wallet/{wallet_address}
GET https://explorer-api.filedgr.network/signatures/{signature_id}
Each signature carries the signing wallet, the file's CID, the anchoring tx_hash and the ledger —
enough for a third party to re-check the hash against IPFS and the transaction against a public
block explorer.
Watching for completion
Signing settles asynchronously. Subscribe to the signature.completed and signature.error
webhook events rather than polling.
signature.failed is accepted as a subscription but never fires — the pipeline's failure status is
ERROR, which maps to signature.error.
Full reference
- Signatures — concepts and rejection rules
- Proof Packages — bundle signatures into shareable evidence
- API Reference