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

FieldTypeDescription
versionintegerSchema version. Always 1 for the current format. Used by questio to detect schema mismatches.
issuerstringHuman-readable name of the document issuer, e.g. "Three UK" or "HMRC". Used in output and matching.
slugstringURL-safe lowercase identifier derived from issuer, e.g. "three-uk". Used as the filename (<slug>.json).
createdstringISO 8601 UTC timestamp of when this fingerprint was first created.
updatedstringISO 8601 UTC timestamp of the last merge. Advances on each questio learn run for this issuer.
sample_countintegerNumber 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

FieldTypeDescription
producerstring[]PDF Producer metadata strings seen across all training samples. Typically the software that generated the PDF, e.g. "iText 7.1.14".
creatorstring[]PDF Creator or CreatorTool metadata strings. Often the application that authored the source document, e.g. "Microsoft Word 16.0".
pdf_versionstring[]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)

FieldTypeDescription
fonts.namesstring[]Font names seen in the document, with 6-character subset prefixes stripped. E.g. "ABCDEF+Arial" is stored as "Arial".
fonts.type_countsobjectMap of font type to maximum count seen across all samples. Possible keys: "Type1", "TrueType", "CIDFont", "Type3", "OpenType".
fonts.subset_behaviourstringConsistency 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)

FieldTypeDescription
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_xmpbooleanWhether this issuer's documents include XMP metadata blocks.
structure.has_obj_streamsbooleanWhether this issuer uses PDF object streams (a PDF 1.5+ compression feature used by some generators).
structure.encryptionstringEncryption 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.

FieldTypeDescription
content_stream.operator_countsobjectMap 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_positioningstringThe most common text positioning command. Values: "Td", "TD", "Tm", "T*". Word uses Tm; many purpose-built PDF generators use Td.
content_stream.font_ref_patternstringHow fonts are referenced in content streams. E.g. /F1, /R7, or /ABCDEF+Helvetica. Generator-specific.
content_stream.dominant_precisionstringCoordinate decimal precision used for text placement. Values: "0dp", "1dp", "2dp", "3dp", "4dp+".
content_stream.dominant_tfstringFormat 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_versionUnion of all seen values across samples (unique values only).
fonts.namesUnion of all seen font names.
fonts.type_countsPer-type maximum across all samples.
structure.objects_per_page, size_per_page_kbRanges expand: min of all mins, max of all maxes.
content_streamReplaced by the new sample's values if sample_count is 2 or fewer. Stabilises on early samples.
sample_countIncremented by 1 on each merge.
updatedSet 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_intervals replacing flat {"min": ..., "max": ...} ranges with statistically-derived bounds
  • ignored_fields list to mark fields the issuer is known to vary legitimately (e.g. document timestamps, dynamic page counts)
  • schema_version as 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.

  1. Choose a slug: lowercase, hyphen-separated, alphanumeric only. This will be the filename.

  2. Create the file at ~/.questio/fingerprints/<slug>.json.

  3. Fill in what you know. Fields you leave empty or omit will not be checked during verification. Partial fingerprints are valid.

  4. Set sample_count: 0 to 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.