DEA Form 106 Theft & Loss Reporting

Filing DEA Form 106 for controlled-substance theft or loss under 21 CFR 1301.76 — discovery timeline, triggers, and immutable ledger-snapshot figures

When a controlled substance goes missing, the registrant carries a strict-liability obligation to notify the Drug Enforcement Administration and to account for the loss on DEA Form 106 (Report of Theft or Loss of Controlled Substances). The governing rule, 21 CFR § 1301.76(b), requires notification of the local DEA field office upon discovery of a theft or significant loss and submission of the completed Form 106 documenting the circumstances. This reference sits within Regulatory Reporting & DEA Submissions and treats the report not as a form a human types from memory, but as a deterministic artifact generated from a frozen point-in-time view of the perpetual inventory. The reported quantity must be the number the ledger held at the moment of discovery — not a figure that drifts as staff continue to dispense during the investigation.

Regulatory Context & Compliance Boundaries

Form 106 exists at the intersection of two obligations that a pharmacy platform must keep structurally separate: the notification duty (tell the DEA promptly) and the accounting duty (report an exact, defensible quantity). The relevant clauses each impose an engineering constraint at the point where a discrepancy is confirmed.

Regulation Reporting requirement Implementation control
21 CFR § 1301.76(b) Notify DEA field office upon discovery of theft/significant loss; submit Form 106 Discovery event freezes a ledger snapshot; notification and filing both reference its hash
21 CFR § 1301.74(c) Report suspicious or unexplained losses; distinguish theft from in-transit loss Trigger classifier separates confirmed theft, significant loss, and carrier/in-transit loss
21 CFR § 1304.04 Retain the report and its supporting records, readily retrievable Frozen snapshot and generated payload persisted to WORM storage with the audit hash
21 CFR § 1304.11 Perpetual inventory must reconcile; a loss is an accountable event Reported quantity is computed from the append-only ledger, never hand-keyed

The decisive principle is that the figure on a Form 106 is a claim about a specific historical state of the inventory. If that state is mutable — if the on-hand count keeps moving while the pharmacist investigates — the reported number cannot be reproduced, and an inspector who recomputes it will find a discrepancy in the discrepancy report itself. The safeguard is to bind every reported quantity to an immutable snapshot taken at discovery. That snapshot is the same append-only record whose scope is fixed by the audit boundary definition, and the triggering signal typically originates in the diversion alert thresholds subsystem that flags an anomalous variance before a human ever counts a shelf.

What Triggers a Form 106

Not every counting discrepancy is a reportable event, and treating them identically either floods the DEA with noise or buries a real theft. The rule distinguishes three categories, and the classifier that routes a variance must encode the distinction explicitly.

Trigger category Definition Reporting path
Confirmed theft Evidence of break-in, employee diversion, robbery, or armed theft Immediate field-office notification; Form 106 filed
Significant loss Unexplained shortage that is material given schedule, quantity, and pattern Field-office notification; Form 106 filed
In-transit loss Shortage where the supplier/common carrier had custody Supplier reports; receiving registrant documents but does not double-file

The “significant loss” test in 21 CFR § 1301.74(c) is deliberately qualitative — the registrant must consider the actual quantity, the schedule of the substance, whether the loss can be associated with a specific cause, and the local pattern of losses. A single Schedule II tablet unaccounted for after a documented spill is not the same as a recurring Schedule II shortfall with no explanation. The system’s job is not to make the legal judgment automatically but to surface the evidence deterministically: the exact variance, the schedule, the recent loss history for that National Drug Code (NDC), and whether custody had transferred. A responsible officer then decides, and that decision is itself logged.

Form 106 lifecycle state machine from variance detection to filing A variance is detected against the perpetual inventory, which places the affected National Drug Code on an inventory hold and freezes a point-in-time ledger snapshot. An investigation follows. If the shortage is explained and reconciled, the state machine branches down to a closed, no-report outcome that is still logged. If the loss is confirmed as theft or a significant loss under 21 CFR 1301.74(c), the state advances to Form 106 generation, where the reported quantity is computed from the frozen snapshot and bound to its SHA-256 hash, and finally to filing with the DEA field office under 21 CFR 1301.76(b). Variance Detected Inventory Hold Investigation Confirmed Loss Form 106 Filed with DEA ledger vs count freeze snapshot classify cause theft / significant from frozen snap field office §1304.11 §1304.04 §1301.74(c) significant? SHA-256 bind §1301.76(b) Reconciled — no report explained shortage, logged audit-logged closure explained
A detected variance freezes a snapshot before any human touches the shelf; the investigation either reconciles to a logged closure or advances to a Form 106 whose reported quantity is computed from — and hashed to — that frozen snapshot.

The Discovery-to-Filing Timeline

The regulatory clock starts at discovery, and the system’s most important job is to make discovery an unambiguous, timestamped event rather than a vague window. The deterministic workflow below runs from the first anomalous signal to the filed report.

  1. Detect the variance. A perpetual-inventory reconciliation or a diversion threshold alert flags that the ledger-derived on-hand quantity for an NDC diverges from a physical count beyond tolerance.
  2. Freeze the snapshot. At the discovery timestamp, capture an immutable point-in-time view of the ledger for the affected NDC(s): sequence number, on-hand balance, and the hash of the last committed event. Nothing about this snapshot changes afterward.
  3. Place the hold. Quarantine the affected stock location so subsequent dispensing does not further move the balance the report will cite. Operations continue elsewhere.
  4. Classify the trigger. Determine whether the facts fit confirmed theft, significant loss, or in-transit loss, gathering the schedule, recent loss history, and custody status as structured evidence.
  5. Notify the field office. For a confirmed theft or significant loss, notify the local DEA office upon discovery per 21 CFR § 1301.76(b); the notification references the snapshot hash so the number spoken on the phone matches the number that will be filed.
  6. Generate the report payload. Compute the reported quantity from the frozen snapshot and assemble the Form 106 field payload deterministically.
  7. Bind and persist. Bind the payload to the SHA-256 hash of the source snapshot, write both to retention storage, and file the report.
  8. Reconcile the ledger. Post a compensating loss event so the perpetual inventory reflects the accounted-for shortage going forward — a new forward event, never an edit to history.

The step that engineers most often collapse is the separation of steps 2 and 6. Freezing the snapshot at discovery and generating the payload later are distinct moments; between them, a pharmacist may dispense a hundred other units. Only a snapshot taken at discovery guarantees the reported quantity is the one that was true when the loss was found.

Form 106 Data Requirements

The form itself asks for a compact set of fields, but each maps to data the system must already hold in a normalized form. Assembling them from the ledger — rather than from a technician’s recollection — is what makes the report reproducible.

Form 106 field Source of truth Notes
Registrant name, address, DEA number Registration profile Must match the DEA registration on file
Date of theft/loss (or discovery) Snapshot discovery timestamp UTC internally; localized for the form
Principal business activity / type of registrant Registration profile Pharmacy, distributor, etc.
Whether police were notified Incident record Required for theft; captured as structured boolean + case reference
Type of loss Trigger classifier Night break-in, employee pilferage, in-transit, etc.
Drug name, strength, dosage form, NDC Normalized product record NDC in canonical 11-digit form
Quantity (in dosage units) Frozen ledger snapshot The on-hand at discovery minus the physical count
Cost code / circumstances narrative Investigation record Free text, but the numeric fields must tie to the snapshot

The quantity field is the one with strict-liability weight. It is not “about a bottle” — it is a count of dosage units that must reconcile against the perpetual inventory the DEA can reconstruct independently. Sourcing it from an immutable snapshot means that when an inspector replays the ledger to the discovery timestamp, they arrive at exactly the number on the form. Any other approach invites a second discrepancy layered on top of the first.

Production Python Implementation

The function below assembles a Form 106 payload from a frozen ledger snapshot. The snapshot is treated as immutable input; the reported quantity is derived, never supplied; and the resulting payload is bound to a SHA-256 hash of the canonical snapshot so the report is non-repudiable. Logging is structured and carries no protected health information (PHI) — only NDC, schedule, quantities, and hashes.

python
from __future__ import annotations

import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum

logger = logging.getLogger("pharmacy.reporting.form106")


class LossType(str, Enum):
    """Trigger categories per 21 CFR § 1301.74(c) / § 1301.76(b)."""
    THEFT = "THEFT"
    SIGNIFICANT_LOSS = "SIGNIFICANT_LOSS"
    IN_TRANSIT = "IN_TRANSIT"


class Form106Error(Exception):
    """Raised when a snapshot cannot support a defensible report."""


@dataclass(frozen=True)
class LedgerSnapshot:
    """Immutable point-in-time view captured at discovery. Never mutated."""
    ndc_11: str                  # canonical 11-digit NDC
    schedule: str                # "II".."V"
    on_hand_at_discovery: int    # dosage units the ledger held at discovery
    last_sequence: int           # monotonic sequence of the last folded event
    last_event_hash: str         # audit-chain hash of that last event
    discovery_ts: str            # ISO-8601 UTC discovery timestamp


@dataclass(frozen=True)
class Form106Payload:
    registrant_dea: str
    ndc_11: str
    schedule: str
    loss_type: str
    reported_quantity: int       # dosage units reported lost
    discovery_ts: str
    police_notified: bool
    snapshot_hash: str           # SHA-256 of the source snapshot (non-repudiation)
    generated_ts: str
    fields: dict = field(default_factory=dict)


def _snapshot_hash(snap: LedgerSnapshot) -> str:
    """Canonical SHA-256 over the snapshot so the report binds to one exact state."""
    canonical = json.dumps(
        {
            "ndc_11": snap.ndc_11,
            "schedule": snap.schedule,
            "on_hand_at_discovery": snap.on_hand_at_discovery,
            "last_sequence": snap.last_sequence,
            "last_event_hash": snap.last_event_hash,
            "discovery_ts": snap.discovery_ts,
        },
        sort_keys=True,
        separators=(",", ":"),
    )
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def build_form_106(
    snapshot: LedgerSnapshot,
    physical_count: int,
    registrant_dea: str,
    loss_type: LossType,
    police_notified: bool,
) -> Form106Payload:
    """Assemble a Form 106 payload from a frozen snapshot and a physical count.

    The reported quantity is DERIVED (on-hand at discovery minus physical count),
    not accepted as input, so the number on the form cannot drift from the ledger
    state the DEA can independently reconstruct under 21 CFR § 1304.11.
    """
    if physical_count < 0:
        raise Form106Error("Physical count cannot be negative.")
    if physical_count > snapshot.on_hand_at_discovery:
        # A surplus is not a loss — route it to overage investigation, not Form 106.
        raise Form106Error(
            "Physical count exceeds ledger on-hand; this is an overage, not a loss."
        )

    reported_quantity = snapshot.on_hand_at_discovery - physical_count
    if reported_quantity == 0:
        raise Form106Error("No shortage: reported quantity is zero, do not file.")

    # Theft always requires police notification to be defensible on the form.
    if loss_type is LossType.THEFT and not police_notified:
        raise Form106Error("Theft requires police notification before filing.")

    snap_hash = _snapshot_hash(snapshot)
    payload = Form106Payload(
        registrant_dea=registrant_dea,
        ndc_11=snapshot.ndc_11,
        schedule=snapshot.schedule,
        loss_type=loss_type.value,
        reported_quantity=reported_quantity,
        discovery_ts=snapshot.discovery_ts,
        police_notified=police_notified,
        snapshot_hash=snap_hash,
        generated_ts=datetime.now(timezone.utc).isoformat(),
        fields={
            "on_hand_at_discovery": snapshot.on_hand_at_discovery,
            "physical_count": physical_count,
            "last_sequence": snapshot.last_sequence,
        },
    )
    # PHI-free structured audit line — identifiers, quantities, and hashes only.
    logger.info(
        "form106_generated ndc=%s schedule=%s loss_type=%s qty=%d "
        "snapshot_hash=%s seq=%d",
        payload.ndc_11, payload.schedule, payload.loss_type,
        payload.reported_quantity, snap_hash, snapshot.last_sequence,
    )
    return payload


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    snap = LedgerSnapshot(
        ndc_11="00093310501",
        schedule="II",
        on_hand_at_discovery=420,
        last_sequence=91827,
        last_event_hash="a91f...c30d",
        discovery_ts="2026-07-16T14:05:00+00:00",
    )
    report = build_form_106(
        snapshot=snap,
        physical_count=380,
        registrant_dea="BX1234567",
        loss_type=LossType.SIGNIFICANT_LOSS,
        police_notified=False,
    )
    print(report.reported_quantity, report.snapshot_hash[:12])

The generator refuses to file a zero shortage, refuses to treat an overage as a loss, and refuses to file a theft without police notification — three guardrails that keep a malformed report from ever reaching the DEA. Because the reported quantity is computed and the payload is hashed to the snapshot, the report is reproducible by anyone who holds the same frozen input. The mechanics of computing the exact quantity and binding the hash are elaborated in automating Form 106 from ledger snapshots.

Compliance Mapping & Audit Boundaries

The generated payload is not the legal artifact by itself — the snapshot plus the payload plus the hash is. Each Form 106 the system produces binds its reported quantity to a specific last_sequence and last_event_hash, which are pointers into the append-only ledger. An inspector can take the snapshot_hash, recompute it from the retained snapshot, and confirm the payload has not been altered since generation; they can then replay the ledger to last_sequence and confirm the on-hand figure the snapshot claims. This two-step verification is what elevates the report from an assertion to evidence.

The boundary rules matter here: the snapshot, the payload, and the compensating loss event all live inside the regulated audit scope, isolated from operational telemetry, and inherit its access controls and WORM retention. A correction to a filed report is never an in-place edit — it is a new payload that references the original by hash, preserving the complete lineage from first discovery to final amended figure.

Error Handling & Offline Resilience

Theft and loss do not wait for connectivity, and a discovery made while a site is offline still starts the regulatory clock. The system degrades deterministically:

  • Snapshot unavailable at discovery. If the ledger cannot be reached to freeze a snapshot, the discovery event is queued locally with its timestamp, and the snapshot is materialized on reconnect from the last locally-durable sequence. The discovery time is preserved even though the payload is generated later.
  • Overage instead of shortage. A physical count above the ledger on-hand is not a Form 106 event; the generator raises rather than filing a negative loss, and the case routes to overage reconciliation.
  • Ambiguous custody. When it is unclear whether a supplier or carrier held the goods, the record is flagged IN_TRANSIT pending confirmation; the receiving registrant documents the shortfall but does not double-file a report the supplier owns.
  • Duplicate filing on retry. The snapshot_hash is the idempotency key for the report. A retried submission carrying the same hash is recognized as the same report, not a second loss, preventing a phantom doubling of the reported quantity.

Every one of these paths preserves the discovery timestamp and the frozen figure, so a delayed or retried filing still reports the number that was true when the loss was found.

Downstream Integration

A filed Form 106 is an input to several other subsystems, not a terminal document:

  1. Perpetual inventory reconciliation. The compensating loss event posted after filing keeps the ongoing count honest and is exactly the kind of event the biennial reconciliation must fold; see Biennial Inventory Reconciliation.
  2. Diversion analytics. A confirmed theft or significant loss feeds back into the loss-history features the diversion alert thresholds engine uses to weigh future variances for the same NDC or location.
  3. Retention and audit indexing. The snapshot, payload, and hash are indexed into the WORM store defined by the audit boundary, where they remain readily retrievable for the retention horizon under 21 CFR § 1304.04.

By generating the report from an immutable snapshot and binding it cryptographically, the pipeline turns a stressful, error-prone manual filing into a reproducible artifact that survives inspection.

Frequently Asked Questions

When does the DEA reporting clock actually start?

At discovery — the moment the registrant becomes aware of a theft or significant loss, not the moment the loss occurred. 21 CFR § 1301.76(b) requires notifying the field office upon discovery and submitting Form 106 to document the circumstances. Making discovery a single timestamped event that freezes a snapshot is what lets the system prove when the clock started and what the inventory looked like at that instant.

How do we decide whether a loss is “significant” enough to report?

The test in 21 CFR § 1301.74(c) is qualitative: weigh the actual quantity, the schedule, whether the loss ties to a specific cause, and the local pattern of losses. The system does not make the legal call automatically; it assembles that evidence deterministically — exact variance, schedule, recent loss history, custody status — so a responsible officer decides on complete facts, and that decision is itself logged.

Why must the reported quantity come from a frozen snapshot rather than a live count?

Because dispensing continues during an investigation. If the report reads a live balance, the number changes between discovery and filing and can never be reproduced. A snapshot taken at discovery fixes the on-hand figure so that when an inspector replays the ledger to that timestamp, they arrive at exactly the quantity on the form.

What is the difference between a theft, a significant loss, and an in-transit loss on Form 106?

Theft involves evidence of a break-in, robbery, or diversion and requires police notification. A significant loss is an unexplained material shortage the registrant cannot tie to a benign cause. An in-transit loss occurred while a supplier or common carrier held custody, in which case the supplier typically files and the receiving registrant only documents the shortfall — filing both would double-count the loss.

How does hashing the snapshot help during an audit?

The SHA-256 snapshot_hash binds the report to one exact ledger state. An inspector recomputes the hash from the retained snapshot to prove the payload was not altered after generation, then replays the ledger to the snapshot’s sequence number to prove the on-hand figure is genuine. It converts the report from a claim into independently verifiable evidence.

Explore deeper

Related topics