Anchoring a file
Hash the file, send the hash, get a signed record. Later, hash the file again and compare.
You hash the file on your side and send the hash. The file itself is optional.
The backend takes one document from your monthly quota first, so a rejected request still counts. Then it checks the hash format and whether the hash is already anchored, queues the record and answers 201 with an event_id.
A writer signs a short payload with the Ed25519 key of your organization and writes the row to the event ledger.
To verify later, Invoance needs only the hash. It never needs the file.
| v | Payload version. Always 2. |
|---|---|
tenant_id | Your tenant id. |
actor_type | Always api_key. |
actor_id | The id of the API key that anchored the file. |
event_type | As sent, or document_anchor. |
document_hash_hex | The hash as stored, lowercase. |
| ts | The created_at value from the 201 response. It differs from the created_at that get returns, which is the row's anchored_at. |
Compact JSON in this order, signed as is with Ed25519. get returns it as signed_payload_b64, next to signature_b64 and public_key_b64.
Hash
SHA-256 over the file bytes, written as 64 hex characters.
Send
POST the hash. Add a reference, an event type, metadata, the file itself or a trace id if you want them.
Check
One document leaves your monthly quota. The hash is validated. A hash already anchored for your tenant gets 409.
Sign and store
A writer signs the payload with your Ed25519 key, writes the ledger row and stores the original if you sent one.
Verify
Hash the file again and call verify, or open the public proof page. Only the hash travels.
Hash the raw bytes of the file with SHA-256. Do not hash a base64 string or a text copy.
The digest is 64 hex characters. The backend trims it and lowercases it before validation and storage. Anything else gets 400 invalid_document_hash.
One changed byte gives a different digest, so keep the exact file you hashed.
Every SDK has a file helper that reads the file, hashes it and calls anchor for you. See the anchor endpoint.
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
const bytes = readFileSync("./INV-2026-0917.pdf");
const documentHash = createHash("sha256").update(bytes).digest("hex");
console.log(documentHash); // 64 lowercase hex characters
Include original_bytes_b64 when you want Invoance to keep a copy you can download later. Leave it out to anchor the hash only.
It must be standard base64 with padding, else 400 invalid_original_bytes_base64. The backend decodes it and rejects an empty result and anything over 5 MiB.
The decoded bytes must hash to document_hash, or the request gets 400 original_bytes_hash_mismatch.
- The handler validates the bytes and passes them to the writer. It does not store them.
- The object key is tenants/{tenant_id}/documents/{yyyy}/{mm}/{dd}/{event_id}.bin in the documents bucket.
- The writer decodes the bytes, checks the hash again and uploads them to object storage.
- It records the object key, the byte size and your plan's retention_days, and adds the size to your storage usage.
- No worker acts on that retention today: document records and originals are neither sealed nor removed when it ends.
document_hash | exactly 64 hexadecimal characters (a SHA-256 digest) |
|---|---|
| metadata | up to 8,192 bytes once serialized as JSON |
original_bytes_b64 | 1 byte to 5 MiB after base64 decoding, and the decoded bytes must hash to document_hash |
| Request body | 2 MB, the framework default; the API routes set no override, so an uploaded original is capped at about 1.5 MB decoded in practice |
document_ref | no length check; only the request body limit applies. Defaults to Untitled |
- The customer API router sets no body limit, so the framework default of 2 MB applies before the handler runs.
- Base64 adds a third, so a body that fits carries about 1.5 MB of file. The 5 MiB check is never reached above that.
- A larger body gets a plain-text 413, not the JSON error shape.
- Anchor larger files hash only and keep the original yourself.
The response body is the file exactly as uploaded, not JSON. There is no file name; use document_ref as yours.
has_original on get and list tells you whether one exists before you call.
| Path | GET /v1/document/{event_id}/original |
|---|---|
| Key scope | read, or write |
| Body | The file exactly as uploaded |
| Content-Type | application/octet-stream |
| Content-Disposition | inline, with no file name |
404 document_not_found | No original was uploaded, the record belongs to another tenant, or it is not active |
| Quota | None consumed |
curl https://api.invoance.com/v1/document/7c1e4b52-9a0f-4d2e-b6f3-2f8a61c0d9e4/original \
-H "Authorization: Bearer $INVOANCE_API_KEY" \
-o ./INV-2026-0917.pdf
Hash the file you hold now, the same way as before. Then pick a check.
POST /v1/document/{event_id}/verify | With your key. Returns match_result, both hashes, anchored_at, document_ref and the organization. Takes one API verification from your quota. Does not check the signature. |
|---|---|
POST /v1/proof/{event_id}/verify | No key. Returns match_result and signature_valid with both hashes and anchored_at. Recorded as a verification for the issuer. |
www.invoance.com/proof/document/{event_id} | No account. Hashes the file in the browser and sends only the hash to the public verify call. |
9b74c98e…5d21af069b74c98e…5d21af06Hashes match41e07fa2…c8836b1dmatch_result | True when the hash you sent equals the anchored hash byte for byte. The file you hold is the file that was anchored. |
|---|---|
signature_valid | True when the stored signature verifies over the signed payload with the key registered for the tenant. False when no key is registered. Public verify only. |
To check the signature yourself, fetch the record with get and verify signature_b64 over signed_payload_b64 with public_key_b64.
event_idThe anchor checked.
match_resultTrue when the submitted hash equals the anchored document hash.
signature_validTrue when the stored signature verifies over the signed bytes with the tenant's registered key; false when no key is registered.
anchored_hashThe document hash stored at anchor time.
submitted_hashThe hash compared, lowercased.
anchored_atWhen the document was anchored.
{
"document_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}