Skip to main content

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

  1. Build the statement — the file's hash and CID, the anchoring transaction, a nonce and a timestamp.
  2. Serialise it as canonical JSON with sorted keys.
  3. Take the keccak-256 digest of those bytes.
  4. 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
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 or with exactly six digits, and sign that exact string.

Fixed values

Several fields are literals. 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 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.

note

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

warning

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.

note

signature.failed is accepted as a subscription but never fires — the pipeline's failure status is ERROR, which maps to signature.error.

Full reference