Mobile App DVIR Export Integration
The mobile app is where the federal record is born, and a Driver Vehicle Inspection Report that leaves a driver’s phone but never lands intact in the compliance store is, to a DOT auditor, a report that was never prepared. Under 49 CFR § 396.11(a) the driver must prepare a report at the completion of each day’s work, and under § 396.11©(2) the carrier must certify any safety-affecting defect corrected before the vehicle is dispatched again — obligations that both depend on the export layer capturing the driver identity, VIN, UTC timestamp, per-component condition, defect entries, and signature without silent loss. This page specifies the mobile export integration as the primary structured-input channel into the DVIR Ingestion & Digital/Paper Parsing Workflows pipeline: the client-side capture and buffering contract, the envelope the app must emit, the validation gate that admits or rejects it, and the idempotency and cryptographic guarantees that make the resulting record legally defensible.
Because native mobile submissions arrive as structured JSON, they bypass the PDF & Image OCR Pipeline Setup entirely and flow straight to synchronous validation — this is the highest-fidelity, lowest-latency door into the system, and the export contract exists to keep it that way.
Prerequisites and Environment Setup
Anchor link to "Prerequisites and Environment Setup"The gateway that receives mobile exports is a pure validation-and-acknowledge service; enrichment and persistence happen downstream. Target Python 3.10+ (the code below uses match statements, the X | None union operator, and StrEnum) with:
pydantic(v2) — declarative schema enforcement and OpenAPI generation for the export contract.fastapi+uvicorn— the ingest endpoint that terminates the mobile HTTPS connection and returns202 Accepted.cryptography— Ed25519 / HMAC-SHA256 verification of the driver certification signature.redis(or any durable KV store) — the idempotency-key ledger used to deduplicate retried submissions.tenacity— bounded retry/backoff on the mobile client and on gateway-to-queue handoff.
The mobile client itself must persist an offline draft store (SQLite or an encrypted key-value store) and an outbox queue. The extracted-field contract the envelope carries is owned upstream by the Standardized DVIR JSON Schema Design; the mobile layer adds only the transport metadata — the client-generated idempotency key, the source timezone, and the signature — that the gate needs to make an admit/reject decision.
Data Schema and Normalization
Anchor link to "Data Schema and Normalization"Every mobile submission must present the same envelope, so the gate has exactly one shape to validate and the audit store has exactly one shape to persist. The envelope wraps the canonical DVIR fields together with the transport metadata that governs deduplication and non-repudiation.
| Field | Type | Enumeration / format | Compliance tag |
|---|---|---|---|
dvir_id |
string |
client-generated UUIDv4; the idempotency key | Deduplication key — prevents a second record for one inspection |
channel |
enum |
mobile |
Provenance marker; mobile bypasses OCR |
vin |
string |
17-char ISO 3779, uppercase alphanumeric | § 396.11(a) vehicle identity |
driver_id |
string |
UUID or DOT-qualified identifier | § 396.11(a) operator identity |
inspection_type |
enum |
pre_trip, post_trip |
§ 396.11(a) end-of-day report is post_trip |
inspected_at |
datetime |
ISO 8601 with offset, coerced to UTC | § 396.11 preparation timestamp |
source_timezone |
string |
IANA zone name (e.g. America/Chicago) |
Forensic context for temporal audit |
odometer |
integer |
non-negative | Correlates with HOS / maintenance records |
defects |
array[DefectEntry] |
see below | § 396.11(a)(3) defect capture |
defect_status |
enum |
no_defects, defects_noted, out_of_service |
§ 396.11©(2) certification trigger |
certification_sig |
string |
base64 Ed25519 or HMAC-SHA256 | Non-repudiation of driver certification |
Each DefectEntry carries a normalized component identifier, a free-text description, an optional defect_code (assigned downstream, may be null at export), a boolean safety_affecting flag mapped to § 396.11(a)(3), and optional media_refs[] pointing to defect photos uploaded out-of-band. Normalize timestamps to UTC at the export boundary and preserve the source zone as metadata — never silently rewrite a timestamp, because a defensible record must show what the device reported.
from datetime import datetime
from enum import StrEnum
from zoneinfo import ZoneInfo
from pydantic import BaseModel, Field, field_validator
class Channel(StrEnum):
MOBILE = "mobile"
class InspectionType(StrEnum):
PRE_TRIP = "pre_trip"
POST_TRIP = "post_trip" # the § 396.11(a) end-of-day report
class DefectStatus(StrEnum):
NO_DEFECTS = "no_defects"
DEFECTS_NOTED = "defects_noted"
OUT_OF_SERVICE = "out_of_service"
class DefectEntry(BaseModel):
component: str # normalized component key, e.g. "service_brakes"
description: str
defect_code: str | None = None # assigned downstream; null at export
safety_affecting: bool # § 396.11(a)(3)
media_refs: list[str] = Field(default_factory=list)
class MobileExportEnvelope(BaseModel):
"""The single shape every mobile submission must produce at the gate."""
dvir_id: str = Field(min_length=8) # client UUID; the idempotency key
channel: Channel = Channel.MOBILE
vin: str = Field(min_length=17, max_length=17)
driver_id: str
inspection_type: InspectionType
inspected_at: datetime
source_timezone: str
odometer: int = Field(ge=0)
defects: list[DefectEntry] = Field(default_factory=list)
defect_status: DefectStatus
certification_sig: str
@field_validator("vin")
@classmethod
def vin_shape(cls, v: str) -> str:
if len(v) != 17 or not v.isalnum():
raise ValueError("VIN must be exactly 17 alphanumeric characters")
return v.upper()
@field_validator("inspected_at")
@classmethod
def to_utc(cls, v: datetime, info) -> datetime:
tz = info.data.get("source_timezone")
if v.tzinfo is None and tz:
v = v.replace(tzinfo=ZoneInfo(tz))
return v.astimezone(ZoneInfo("UTC"))
Core Workflow: Resilient Client Export and Gateway Admission
Anchor link to "Core Workflow: Resilient Client Export and Gateway Admission"The export workflow has two halves: the mobile client, which must survive dead zones and duplicate sends, and the gateway, which must admit exactly one immutable record per inspection.
Client side — buffer, then flush. The app writes every completed inspection to its offline draft store with a locally generated dvir_id before any network call. A background flush drains the outbox with exponential backoff and randomized jitter so that a fleet returning to coverage at shift-end does not stampede the gateway. Because dvir_id is generated once at capture, a retried submission carries the same key and is deduplicated at the gate rather than creating a second compliance record.
import random
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
@retry(
stop=stop_after_attempt(8),
wait=wait_exponential_jitter(initial=1, max=120, jitter=random.random),
reraise=True,
)
def flush_one(client, envelope: MobileExportEnvelope) -> str:
resp = client.post(
"/v1/dvir/ingest",
content=envelope.model_dump_json(),
headers={
"Idempotency-Key": envelope.dvir_id,
"Content-Type": "application/json",
},
timeout=15.0,
)
if resp.status_code in (200, 202, 409): # 409 = already ingested; treat as success
return envelope.dvir_id
resp.raise_for_status() # 4xx (except 409) / 5xx -> retry or surface
Gateway side — deduplicate, verify, gate, acknowledge. The endpoint checks the idempotency ledger first, verifies the certification signature, validates the envelope against the contract, and only then hands the payload to the asynchronous batch queue. It never blocks the HTTP response on enrichment.
from fastapi import FastAPI, Header, Request, Response
from pydantic import ValidationError
app = FastAPI()
@app.post("/v1/dvir/ingest")
async def ingest(request: Request, idempotency_key: str = Header(...)) -> Response:
# 1. Deduplicate on the client-generated key — one inspection, one record.
if await ledger.seen(idempotency_key):
return Response(status_code=409) # already ingested; safe to retry
raw = await request.body()
# 2. Validate against the strict contract before anything touches the store.
try:
envelope = MobileExportEnvelope.model_validate_json(raw)
except ValidationError as exc:
# Reject the payload; return machine-readable, field-level errors.
return Response(content=exc.json(), status_code=422,
media_type="application/json")
# 3. Verify the driver certification signature (non-repudiation).
if not verify_signature(envelope):
return Response(status_code=401) # reject unsigned / tampered exports
# 4. Admit: record the key, enqueue for async processing, acknowledge fast.
await ledger.record(idempotency_key)
await batch_queue.publish(envelope)
return Response(status_code=202) # 202 Accepted; downstream continues
For high-volume operations, decouple this acknowledgement from the parsers by publishing to a broker so a burst of end-of-shift submissions cannot overrun downstream workers — that handoff is specified in Async Batching for High-Volume Ingestion. The webhook, header-gating, and offline-draft edge cases at the endpoint are covered in depth in Best Practices for Mobile DVIR API Integration.
Compliance Thresholding and Routing
Anchor link to "Compliance Thresholding and Routing"The export layer makes exactly one compliance decision — admit or reject — but it also sets the flags that the downstream engine acts on. Map each gate outcome to an imperative action rather than a passive “flag for review”:
- Missing mandatory field (VIN, driver_id, timestamp, signature): reject the payload with
422and return field-level errors; do not admit a partial record. - Signature absent or invalid: reject the export with
401; an uncertified report cannot satisfy the § 396.11©(2) certification chain. - Timestamp drift > 15 minutes from NTP-synced server clock: admit the record but tag it for manual temporal review; never silently rewrite the driver’s reported time.
defect_status = out_of_service: admit, then immediately signal a hold — the vehicle must not be dispatched until repair is certified per § 396.11©(2).- Any
safety_affecting = truedefect: admit and forward to downstream severity scoring; a safety-affecting defect must escalate, not default to low priority.
Once admitted, the normalized defects flow to the Severity Scoring Algorithms for DVIR Defects engine, which resolves each defect to a reproducible 0–100 score. Those scores map to the same severity bands used across the pipeline — 0–34 minor, 35–69 major, 70–100 critical — and a critical band triggers the out-of-service workflow in Critical vs Non-Critical Routing Logic. The export layer’s job is to guarantee the scorer receives a complete, typed, signed record; it must never guess a missing field to keep the pipeline moving.
Production Integration and Platform Synchronization
Anchor link to "Production Integration and Platform Synchronization"Once the gateway returns 202 Accepted, downstream workflows emit events without the mobile client polling. On successful ingestion, publish a dvir.ingested event keyed on dvir_id so fleet management, CMMS, and telematics/ELD platforms react in near-real time — a critical defect can open a repair order in the CMMS and raise a driver alert before the truck leaves the yard.
Idempotency chains across the whole path: the same dvir_id that deduplicates at the gate also keys the downstream event, so a replayed event never opens a duplicate repair order. For evidentiary integrity, hash the raw export bytes and the normalized record with SHA-256 and append both digests to the audit ledger; chain each entry to the prior digest so any tampering breaks the chain. Ed25519 or HMAC-SHA256 signing at the mobile layer plus SHA-256 chaining at ingest together give the non-repudiation a DOT audit expects. Retry logic must be bounded, with permanently failed exports captured in a dead-letter queue and a structured failure manifest for manual reconciliation — a failed export must be visible, never dropped.
Engineering Standards Checklist
Anchor link to "Engineering Standards Checklist"Frequently Asked Questions
Anchor link to "Frequently Asked Questions"How do I prevent duplicate DVIR records when a mobile client retries after a timeout?
Generate the dvir_id once at capture on the device and send it as both the envelope field and the Idempotency-Key header on every retry. The gateway checks the idempotency ledger first and returns 409 for a key it has already recorded, which the client treats as success. One inspection therefore yields exactly one immutable record no matter how many times the flush retries.
Should the mobile app coerce timestamps to UTC before export, or should the gateway?
Coerce at the export boundary using the device’s IANA source timezone, and carry source_timezone as metadata so the original context survives. If the incoming timestamp still drifts more than fifteen minutes from the server’s NTP-synced clock, admit the record but tag it for manual temporal review under § 396.11 — never silently rewrite a driver’s reported time, because that compromises the legal defensibility of the report.
What HTTP status should the gateway return for a payload missing the driver signature?
Reject it with 401, distinct from the 422 used for schema failures. An export without a verifiable driver certification signature cannot support the § 396.11©(2) certification chain, so admitting it would create a record the carrier cannot defend in a DOT audit.
Do native mobile exports go through the OCR pipeline?
No. Native submissions arrive as structured JSON and route directly to synchronous validation, bypassing optical recognition entirely. Only scanned or flattened documents enter the OCR pipeline; keeping the two channels separate prevents OCR latency from blocking real-time digital submissions.