Fingerprint Format Reference (v1)
April 12, 2026 ยท View on GitHub
questio stores issuer fingerprints as JSON files in ~/.questio/fingerprints/. Each file describes the structural signature of a trusted document issuer, built by training questio on one or more genuine documents.
This reference covers every field in the v1 schema, the merge behaviour when multiple samples are trained, migration notes, and a guide to hand-authoring a fingerprint.
Schema overview
{
"version": 1,
"issuer": "Example Bank",
"slug": "example-bank",
"created": "2026-04-10T12:00:00+00:00",
"updated": "2026-04-10T12:00:00+00:00",
"sample_count": 1,
"producer": ["BankPDF Generator 3.2"],
"creator": ["CoreBanking Suite"],
"pdf_version": ["1.6"],
"fonts": {
"names": ["Arial", "Helvetica", "CourierNew"],
"type_counts": {"Type1": 2, "TrueType": 3},
"subset_behaviour": "consistent"
},
"structure": {
"objects_per_page": {"min": 45.0, "max": 60.0},
"size_per_page_kb": {"min": 18.5, "max": 24.0},
"has_xmp": true,
"has_obj_streams": false,
"encryption": "none"
},
"content_stream": {
"operator_counts": {"BT": 45, "ET": 45, "Tf": 20, "Tj": 60, "cm": 5},
"dominant_positioning": "Td",
"font_ref_pattern": "/F1",
"dominant_precision": "2dp",
"dominant_tf": "name_size"
}
}
Field reference
Top-level fields
| Field | Type | Description |
|---|---|---|
version | integer | Schema version. Always 1 for the current format. Used by questio to detect schema mismatches. |
issuer | string | Human-readable name of the document issuer, e.g. "Three UK" or "HMRC". Used in output and matching. |
slug | string | URL-safe lowercase identifier derived from issuer, e.g. "three-uk". Used as the filename (<slug>.json). |
created | string | ISO 8601 UTC timestamp of when this fingerprint was first created. |
updated | string | ISO 8601 UTC timestamp of the last merge. Advances on each questio learn run for this issuer. |
sample_count | integer | Number of genuine documents trained into this fingerprint. Higher values produce wider, more reliable ranges. |
Deviation signal: version mismatch causes questio to skip comparison rather than produce a misleading score.
Metadata arrays
| Field | Type | Description |
|---|---|---|
producer | string[] | PDF Producer metadata strings seen across all training samples. Typically the software that generated the PDF, e.g. "iText 7.1.14". |
creator | string[] | PDF Creator or CreatorTool metadata strings. Often the application that authored the source document, e.g. "Microsoft Word 16.0". |
pdf_version | string[] | PDF specification versions seen, e.g. "1.4", "1.6", "1.7", "2.0". |
Deviation signal: if a suspect document's Producer does not appear in the fingerprint's producer array, questio flags it as a producer mismatch. For a bank statement whose fingerprint only lists a known banking platform, seeing LibreOffice or Adobe Acrobat in the producer field is a strong signal.
Font fields (fonts)
| Field | Type | Description |
|---|---|---|
fonts.names | string[] | Font names seen in the document, with 6-character subset prefixes stripped. E.g. "ABCDEF+Arial" is stored as "Arial". |
fonts.type_counts | object | Map of font type to maximum count seen across all samples. Possible keys: "Type1", "TrueType", "CIDFont", "Type3", "OpenType". |
fonts.subset_behaviour | string | Consistency of 6-character subset prefix tags across the document. Values: "consistent" (single prefix set), "inconsistent" (multiple prefix sets, indicating multiple editing sessions), "mixed", "none". |
Deviation signal: CIDFont appearing in type_counts when the fingerprint has never seen CIDFont objects fires the Acrobat edit signature check. Acrobat's Edit Text tool is the primary source of CIDFont injections in documents that did not originally contain them. A separate font session check fires if subset_behaviour is "inconsistent" in a document that should be single-session.
Structure fields (structure)
| Field | Type | Description |
|---|---|---|
structure.objects_per_page | {"min": float, "max": float} | Range of PDF object counts per page. Trained across all samples. |
structure.size_per_page_kb | {"min": float, "max": float} | Range of file size (KB) per page. Trained across all samples. |
structure.has_xmp | boolean | Whether this issuer's documents include XMP metadata blocks. |
structure.has_obj_streams | boolean | Whether this issuer uses PDF object streams (a PDF 1.5+ compression feature used by some generators). |
structure.encryption | string | Encryption type seen in training documents. Values: "none", "RC4-40", "RC4-128", "AES-128", "AES-256". |
Deviation signal: significant deviation outside the objects_per_page or size_per_page_kb range is a structural anomaly signal. A document far outside the expected range may have had objects inserted or removed.
Content stream fields (content_stream)
The content stream is the drawing instructions inside a PDF. Each PDF generator has a characteristic style, sometimes called its "handwriting". These fields capture that style.
| Field | Type | Description |
|---|---|---|
content_stream.operator_counts | object | Map of PDF operator name to count. Key operators: BT/ET (text block open/close), Tf (font select), Tj/TJ (text show), cm (transformation matrix), q/Q (graphics state push/pop). |
content_stream.dominant_positioning | string | The most common text positioning command. Values: "Td", "TD", "Tm", "T*". Word uses Tm; many purpose-built PDF generators use Td. |
content_stream.font_ref_pattern | string | How fonts are referenced in content streams. E.g. /F1, /R7, or /ABCDEF+Helvetica. Generator-specific. |
content_stream.dominant_precision | string | Coordinate decimal precision used for text placement. Values: "0dp", "1dp", "2dp", "3dp", "4dp+". |
content_stream.dominant_tf | string | Format pattern of Tf (font select) commands. Values: "name_size" (e.g. /F1 12 Tf), "resource_size", or other. |
Deviation signal: a mismatch between the fingerprint's content stream style and the suspect document's observed style indicates that content was written by a different tool than the declared producer. This is the deepest check and the hardest for a forger to replicate without knowing exactly what pattern to mimic.
Merge behaviour
When questio learn is run a second time for the same issuer, it merges the new sample into the existing fingerprint:
| Field(s) | Merge rule |
|---|---|
producer, creator, pdf_version | Union of all seen values across samples (unique values only). |
fonts.names | Union of all seen font names. |
fonts.type_counts | Per-type maximum across all samples. |
structure.objects_per_page, size_per_page_kb | Ranges expand: min of all mins, max of all maxes. |
content_stream | Replaced by the new sample's values if sample_count is 2 or fewer. Stabilises on early samples. |
sample_count | Incremented by 1 on each merge. |
updated | Set to the current UTC timestamp on each merge. |
A fingerprint trained on one sample uses exact values. Trained on multiple samples, it builds a range. A range-based fingerprint is more reliable because it accounts for normal variation between legitimate documents from the same issuer.
Versioning and migration
v1 is the only current schema version. A hypothetical v2 would likely introduce:
confidence_intervalsreplacing flat{"min": ..., "max": ...}ranges with statistically-derived boundsignored_fieldslist to mark fields the issuer is known to vary legitimately (e.g. document timestamps, dynamic page counts)schema_versionas a dedicated top-level key separate from the fingerprint's content version
Migration path: questio's load_fingerprint() reads the version field on load. A v1-to-v2 migration shim would convert the flat ranges to confidence intervals on first load and write the upgraded file back. Because the fingerprint directory is user-controlled, no automatic migration will silently modify files without a user-visible step.
Hand-authoring a fingerprint
You can create a fingerprint JSON file manually rather than training questio on a genuine document. This is useful when you have metadata knowledge about an issuer but not a sample document.
-
Choose a slug: lowercase, hyphen-separated, alphanumeric only. This will be the filename.
-
Create the file at
~/.questio/fingerprints/<slug>.json. -
Fill in what you know. Fields you leave empty or omit will not be checked during verification. Partial fingerprints are valid.
-
Set
sample_count: 0to indicate the fingerprint was hand-authored rather than trained.
Minimal valid fingerprint:
{
"version": 1,
"issuer": "Acme Utilities",
"slug": "acme-utilities",
"created": "2026-04-10T00:00:00+00:00",
"updated": "2026-04-10T00:00:00+00:00",
"sample_count": 0,
"producer": ["Acme Billing Platform"],
"creator": [],
"pdf_version": ["1.4"],
"fonts": {
"names": [],
"type_counts": {},
"subset_behaviour": "consistent"
},
"structure": {
"objects_per_page": {"min": 0, "max": 999},
"size_per_page_kb": {"min": 0, "max": 9999},
"has_xmp": false,
"has_obj_streams": false,
"encryption": "none"
},
"content_stream": {
"operator_counts": {},
"dominant_positioning": "unknown",
"font_ref_pattern": "unknown",
"dominant_precision": "unknown",
"dominant_tf": "unknown"
}
}
Use wide ranges for objects_per_page and size_per_page_kb when you are not sure of the expected values. Narrow them only when you have reliable data, otherwise you will generate false positives.
After placing the file, run questio verify --issuer acme-utilities suspect.pdf to test it against a document.