Skip to content

Manifests and integrity

A manifest describes every source document of a filing’s current version, with a SHA-256 hash and byte size for each. Verify the hashes to prove that what you stored is what VelociFile served.

Terminal window
curl -sS --fail-with-body "https://api.dev.velocifile.ai/v1/filings/$VF_FILING_ID/versions/$VF_VERSION_ID/manifest" \
-H "Authorization: Bearer $VELOCIFILE_API_KEY"
{
"schema_version": "1.0.0",
"filing_id": "fil_ABCD-123456789",
"filing_version_id": "fv_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"completeness": "complete",
"missing_documents": [],
"rights": {
"agreement_id": "agreement_ref",
"permitted_uses": ["internal_analysis", "product_design", "extraction", "internal_storage"],
"redistribution_permitted": false,
"retain_until": null
},
"documents": [
{
"document_id": "doc_123",
"filename": "rate-manual.pdf",
"media_type": "application/pdf",
"byte_size": 482113,
"sha256": "24788f42af309e5c328101cad446208feed26e3e0caa736125fe364711e00192",
"role": "rate_manual",
"source_uri": "https://...",
"observed_at": "2026-10-20T08:00:00Z"
}
]
}

Document role is one of form, endorsement, rate_manual, underwriting_manual, supporting or unknown. It comes from the SERFF schedule the document was filed under. Correspondence and anything unrecognized is unknown.

rights comes from your agreement and states what you may do with the documents. Honor it.

A manifest covers at most 200 documents. Each PDF is at most 100 MiB, a version at most 1 GiB, and the manifest itself at most 1 MiB. Documents must be PDFs. A version over these limits answers 422 CAPABILITY_UNSUPPORTED with the reason.

VelociFile judges completeness against the attachment list in the filing’s own SERFF record.

  • complete: every attachment the filing lists is in the manifest.
  • partial: one or more listed attachments are not held. missing_documents names each one.
  • completeness and missing_documents are fixed when the version’s first manifest is built. They are not recomputed later, even if the held attachments change afterwards.
  • If the stored filing record has no attachment list, the manifest is partial with one missing item saying the source attachment list is unavailable. VelociFile never claims completeness it cannot check.

Documents that a filing adopts by reference from another filing (for example an ISO manual) are outside this definition. See deviation 4.

The manifest bytes are deterministic: two-space indent, UTF-8, one trailing newline. The version’s manifest_sha256 is the SHA-256 of the exact response body. Hash the raw bytes you received, before any JSON parsing.

GET .../documents/{document_id}/download returns a presigned URL that expires 15 minutes after issuance, the document’s sha256 and its byte_size. The response carries Cache-Control: no-store.

{
"url": "https://...",
"expires_at": "2026-10-22T16:15:00Z",
"sha256": "24788f42af309e5c328101cad446208feed26e3e0caa736125fe364711e00192",
"byte_size": 482113
}

Request the URL, then fetch the PDF immediately. Do not store or share the URL. If the source object changed since the manifest was built, the request answers 410 VERSION_SUPERSEDED: read the filing again.

import hashlib
import os
import httpx
BASE = "https://api.dev.velocifile.ai/v1"
headers = {"Authorization": f"Bearer {os.environ['VELOCIFILE_API_KEY']}"}
filing_id = os.environ["VF_FILING_ID"]
version_id = os.environ["VF_VERSION_ID"]
version_path = f"{BASE}/filings/{filing_id}/versions/{version_id}"
# Fetch the manifest first: building it is what records manifest_sha256 on the version.
manifest_response = httpx.get(f"{version_path}/manifest", headers=headers, timeout=60)
manifest_response.raise_for_status()
version = httpx.get(version_path, headers=headers, timeout=30)
version.raise_for_status()
# 1. The manifest itself: hash the raw response bytes.
expected_manifest_hash = version.json()["manifest_sha256"]
assert expected_manifest_hash is not None, "manifest_sha256 is null after fetching the manifest"
actual = hashlib.sha256(manifest_response.content).hexdigest()
assert actual == expected_manifest_hash, "manifest hash mismatch"
# 2. Each PDF: hash the bytes and compare with the manifest.
for document in manifest_response.json()["documents"]:
issued = httpx.get(
f"{version_path}/documents/{document['document_id']}/download", headers=headers, timeout=30
)
issued.raise_for_status()
pdf = httpx.get(issued.json()["url"], timeout=120) # presigned: send no Authorization header
pdf.raise_for_status()
assert len(pdf.content) == document["byte_size"], f"{document['document_id']}: size mismatch"
digest = hashlib.sha256(pdf.content).hexdigest()
assert digest == document["sha256"], f"{document['document_id']}: hash mismatch"
print("verified", document["document_id"], digest)

On a mismatch, discard the file and fetch it again. If it still does not match, contact VelociFile with the request_id of the manifest call.

At most 10 download issuances can be in flight per key. See Rate limits.