Verification Router Service Architecture: Production-Grade DSCSA Routing Pipelines

Part of the DSCSA Compliance Architecture & Standards Mapping framework, this guide treats the Verification Router Service (VRS) as the interoperability layer that turns a discrete saleable-unit verification request into a standardized, cryptographically verifiable routing decision. Under the Drug Supply Chain Security Act, when a wholesale distributor receives a saleable return or a dispenser needs to confirm a suspect unit, it must ask the originating manufacturer — not a shared database — whether a specific serialized unit is legitimate, and receive an answer in near real time. The operational problem this page solves is precise: how to build a routing intermediary that resolves the correct manufacturer endpoint from a product identifier, enforces transport and message security, degrades gracefully when a partner endpoint is unreachable, and records every decision as an inspector-ready audit artifact — without a single dropped or misrouted request breaking a partner’s ability to verify product.

Architecture Diagram

The VRS is a stateless routing intermediary sitting between a requestor (dispenser or wholesaler) and one or more responder endpoints (manufacturer verification services or authorized third parties). It ingests a verification request keyed on the product identifier, resolves the correct responder, forwards a normalized query, and returns a deterministic verified decision with a standardized reason code. The sequence below is the reference request/response flow the rest of this page implements.

Figure — Verification Router Service request and response routing.

Verification Router Service request and response routing sequence A sequence diagram with three lifelines: the requesting dispenser or wholesaler, the Verification Router, and the manufacturer VRS. Step one, the requestor sends a verification request keyed on GTIN (01), serial (21), lot (10), and expiry (17) to the router. Step two, the router resolves the GTIN Company Prefix to a responder endpoint using longest-prefix matching. Step three, the router forwards a normalized verification request to the manufacturer VRS. Step four, the manufacturer returns a verified true-or-false decision with a standardized reason code. Step five, the router returns the verification response to the requestor. Across the router-to-manufacturer span, rate limiting, circuit breaking, and non-repudiation logging are applied to every hop. Requestor dispenser / wholesaler Verification Router stateless intermediary Manufacturer VRS source of truth verify saleable unit GTIN(01)·serial(21)·lot(10)·exp(17) 1 resolve GTIN prefix → endpoint longest-prefix match 2 routed verification request 3 verified + reason code 4 verification response 5 rate limiting · circuit breaking · non-repudiation logging

Foundational Concepts & Data Contracts

Everything the router does is governed by a small set of identifier and message contracts. Getting these exact is what separates a compliant VRS from one that silently returns no_match on legitimate product.

The verification key. A DSCSA product identifier request carries four elements, encoded on the physical pack via GS1 Application Identifiers and mirrored in the verification payload: the 14-digit GTIN (01), the serial number (21), the expiration date (17) in YYMMDD, and the lot/batch number (10). The GTIN and serial together form the SGTIN that uniquely identifies the unit. The router resolves the responder using the GS1 Company Prefix embedded in the GTIN, so correct GTIN structure — validated upstream in the GS1 Standards Implementation layer — is a hard precondition for routing to work at all.

The routing table. The core data structure is a versioned map from GTIN prefix (Company Prefix, variable length) to a responder endpoint, its certificate identity, and health state. Because GS1 Company Prefixes vary in length, resolution must try the longest prefix first and fall back to shorter ones, exactly as a longest-prefix-match router does for IP subnets.

The response contract. A conformant responder returns a deterministic decision — verified: true or verified: false — accompanied by a standardized reason code. The reason codes the router must recognize and propagate unchanged are:

Reason code Meaning Downstream action
verified Identifier matches an active, commissioned unit Return success to requestor
no_match GTIN + serial pair not found for this manufacturer Escalate to suspect-product review
expired Serial matches but expiration date is past Flag; do not dispense
decommissioned Unit was dispensed, destroyed, or sampled Investigate possible duplicate
recalled Lot is under an active recall Quarantine immediately

Event alignment. A verification event is not an EPCIS event, but the router should correlate each request against the ObjectEvent and TransactionEvent history captured in the EPCIS event ingestion pipeline so that a no_match from a manufacturer can be reconciled against what the supply chain believes it received.

Figure — The VRS component stack: a request descends through four processing layers while the append-only audit sink taps every one.

Layered Verification Router Service component stack A layered architecture diagram. A verification request enters at the top and descends through four processing layers in order: an ingress gateway enforcing TLS 1.2 with mutual authentication, OAuth 2.0 tokens, per-partner rate limiting, and idempotency keys; a routing engine performing longest-prefix matching against a versioned routing table; a message transformer normalizing the payload to canonical reason codes; and a security and identity boundary applying X.509 signing and SHA-256 payload hashing. Below the stack the request exits to the resolved manufacturer responder. On the right, an append-only, hash-chained audit sink runs the full height of the stack, and a dashed tap connects every layer to it so each decision is logged. verification request 1 · Ingress gateway / rate limiter TLS 1.2+ mTLS · OAuth 2.0 · per-partner rate limit · idempotency key 2 · Routing engine longest-prefix match · versioned routing table · endpoint health gate 3 · Message transformer normalize payload · map responder string → canonical reason code 4 · Security / identity boundary X.509 signing · SHA-256 payload hash · non-repudiation dispatch to resolved responder Audit sink append-only hash-chained 21 CFR Part 11

Step-by-Step Implementation

A production VRS pipeline is five tightly coupled layers. The steps below build them in dependency order; each step names the DSCSA or GS1 rule it satisfies.

Step 1 — Enforce the transport and identity boundary (ingress gateway)

Satisfies: FDA interoperability guidance requires authenticated, encrypted, non-repudiable exchange between trading partners. The gateway terminates TLS 1.2+ with mutual authentication, validates an OAuth 2.0 client-credentials token scoped to the partner’s GLN, and applies a sliding-window rate limit per partner. Idempotency keys extracted from request headers prevent a retried request from being billed or logged twice. Payload signing and encryption at this boundary follow the same key-management discipline described in the guide to data security and encryption boundaries.

Step 2 — Model the request as a typed contract

Satisfies: the four mandatory GS1 AIs must be present and well-formed before routing. Enforcing them as a Pydantic v2 model rejects malformed payloads at the edge rather than deep in the pipeline.

from datetime import date
from pydantic import BaseModel, field_validator

class VerificationRequest(BaseModel):
    gtin: str        # (01) GTIN-14
    serial: str      # (21) serial number
    lot: str         # (10) batch / lot
    expiry: date     # (17) expiration date

    @field_validator("gtin")
    @classmethod
    def gtin_is_14_digits(cls, v: str) -> str:
        if len(v) != 14 or not v.isdigit():
            raise ValueError(f"GTIN must be 14 digits, got {v!r}")
        return v

    @field_validator("serial")
    @classmethod
    def serial_present(cls, v: str) -> str:
        if not (1 <= len(v) <= 20):
            raise ValueError("serial must be 1-20 chars per GS1 AI (21)")
        return v

Step 3 — Resolve the responder with longest-prefix matching

Satisfies: DSCSA verification must reach the originating manufacturer. The routing engine resolves the endpoint from the GTIN’s Company Prefix using a cached, versioned table and refuses to route to an unhealthy endpoint.

from dataclasses import dataclass

@dataclass
class RoutingEntry:
    gtin_prefix: str
    endpoint: str
    is_healthy: bool = True

class RouteResolver:
    def __init__(self, routing_table: list[RoutingEntry]):
        self.table = {r.gtin_prefix: r for r in routing_table}

    def resolve(self, gtin: str) -> RoutingEntry:
        # GS1 Company Prefixes vary in length; try longest first.
        for prefix_len in (9, 8, 7, 6, 5):
            entry = self.table.get(gtin[:prefix_len])
            if entry and entry.is_healthy:
                return entry
        raise LookupError(f"No healthy endpoint for GTIN prefix: {gtin[:9]!r}")

Step 4 — Dispatch asynchronously with a circuit breaker

Satisfies: near-real-time response with graceful isolation of unresponsive responders. Python’s asyncio with httpx gives non-blocking I/O and strict timeout boundaries; a per-endpoint circuit breaker trips after consecutive failures so one slow manufacturer cannot exhaust the pool.

import asyncio
import httpx

class CircuitBreaker:
    def __init__(self, threshold: int = 5, reset_after: float = 30.0):
        self.threshold = threshold
        self.reset_after = reset_after
        self.failures = 0
        self.opened_at: float | None = None

    def allow(self) -> bool:
        if self.opened_at is None:
            return True
        if asyncio.get_event_loop().time() - self.opened_at >= self.reset_after:
            self.opened_at = None       # half-open: allow one probe
            self.failures = 0
            return True
        return False

    def record_success(self) -> None:
        self.failures = 0
        self.opened_at = None

    def record_failure(self) -> None:
        self.failures += 1
        if self.failures >= self.threshold:
            self.opened_at = asyncio.get_event_loop().time()

class VRSRouter:
    def __init__(self, resolver: RouteResolver, timeout: float = 5.0):
        self.resolver = resolver
        self.timeout = timeout
        self.breakers: dict[str, CircuitBreaker] = {}

    async def verify(self, client: httpx.AsyncClient,
                     req: VerificationRequest) -> dict:
        entry = self.resolver.resolve(req.gtin)
        breaker = self.breakers.setdefault(entry.endpoint, CircuitBreaker())
        if not breaker.allow():
            raise RuntimeError(f"circuit open for {entry.endpoint}")
        params = {"gtin": req.gtin, "serial": req.serial,
                  "lot": req.lot, "expiry": req.expiry.strftime("%y%m%d")}
        try:
            resp = await client.get(entry.endpoint, params=params,
                                    timeout=self.timeout)
            resp.raise_for_status()
            breaker.record_success()
            return resp.json()      # {"verified": bool, "reason": "..."}
        except (httpx.TimeoutException, httpx.HTTPStatusError):
            breaker.record_failure()
            raise

Step 5 — Normalize the response and hand off to the audit sink

Satisfies: deterministic responses and a complete chain of custody. The transformer maps the responder’s reason string onto the canonical reason-code enum, and every decision — including the resolved endpoint, latency, and a hash of the request — is emitted to the append-only audit sink before the response is returned to the requestor. A no_match or recalled outcome triggers the Suspect Product Investigation Workflows, including the automated DSCSA compliance gap checks that reconcile the response against expected inventory.

Routing tables and certificate stores are refreshed from a secure configuration service (HashiCorp Vault or AWS Parameter Store) without a restart, so onboarding a new manufacturer prefix or rotating an endpoint is a zero-downtime operation.

Validation & Error Handling

Malformed and hostile input is the norm, not the exception, in a multi-partner verification network. The pipeline must reject bad data early and keep running:

  • Schema rejection at the edge. A payload that fails the VerificationRequest model is refused with an HTTP 422 and a structured error; it never reaches the routing engine. This mirrors the discipline covered in schema validation and error handling.
  • Unroutable identifiers. A LookupError from the resolver — no prefix match, or every candidate endpoint circuit-open — is returned as a distinct router_unavailable code, never conflated with a manufacturer’s genuine no_match. Conflating the two would falsely brand legitimate product as suspect.
  • Dead-letter capture. Requests that cannot be processed after retries land in a dead-letter queue with the original payload, the failure reason, and a correlation ID for manual review and compliance reporting.
  • Idempotent retries. Retry logic keys on the idempotency header so a re-sent request returns the cached original decision rather than issuing a duplicate query to the manufacturer.
async def verify_safe(router: VRSRouter, client, req: VerificationRequest,
                      dlq) -> dict:
    for attempt in range(3):
        try:
            return await router.verify(client, req)
        except (httpx.TimeoutException, httpx.HTTPStatusError):
            await asyncio.sleep((2 ** attempt) * 0.1)  # backoff w/ base jitter
        except (LookupError, RuntimeError) as exc:
            return {"verified": None, "reason": "router_unavailable",
                    "detail": str(exc)}
    await dlq.put(req)
    return {"verified": None, "reason": "dead_lettered"}

Performance & Scalability Considerations

Verification traffic is spiky — saleable returns bunch up around distribution cycles — so the router must scale horizontally while protecting downstream manufacturers:

  • Statelessness. Because the router holds no per-request state (the routing table and breakers are shared, replicated caches), nodes scale out linearly behind a load balancer.
  • Connection pooling. A single httpx.AsyncClient with a bounded connection pool per responder amortizes TLS handshakes; unbounded concurrency toward one manufacturer is exactly what the circuit breaker and per-partner rate limits prevent.
  • Batch fan-out. When a wholesaler submits a returns manifest, verify with a bounded asyncio.Semaphore so a 10,000-line manifest does not open 10,000 simultaneous sockets — the same concurrency-control pattern used when building async batch processors for serialization events.
  • Responder rate limits. Manufacturer endpoints publish their own throttling ceilings; the router must respect them and shape traffic accordingly, coordinating with the strategies in handling rate limits on FDA verification APIs.
  • Cache the routing table, version the cache. Reads hit an in-memory snapshot; a background job pulls a new signed version and swaps it atomically, so resolution never blocks on a config fetch.

Audit & Compliance Checkpoints

Every routing decision is a regulated record. At this scope the pipeline must log, retain, and sign the following:

  • Immutable decision log. Each verification — request hash, resolved endpoint, verified outcome, reason code, latency, and correlation ID — is written to an append-only, hash-chained store so no entry can be altered without breaking the chain. Records are retained for six years and reproducible for FDA inspection under 21 CFR Part 11.
  • Non-repudiation. Inbound and outbound payloads are cryptographically signed and the signatures retained, proving which partner asked what and which responder answered. X.509 certificate rotation and SHA-256 payload hashing are managed at the security boundary.
  • Structured telemetry. Latency, circuit-breaker trip rate, and schema-rejection counts are exported via OpenTelemetry; alerts fire when the routing failure rate exceeds 0.5% or a responder certificate is within its expiration window.
  • Traceable escalation. When a no_match, expired, or recalled decision is returned, the audit record links to the investigation case it triggered, giving an inspector an unbroken trail from verification request to remediation.

Figure — The per-endpoint circuit-breaker state machine that isolates an unresponsive responder.

Per-endpoint circuit-breaker state machine A finite state machine with three states. The breaker starts CLOSED, letting requests through and counting consecutive failures. When failures reach the threshold of five, it transitions to OPEN, where every request fast-fails while a cooldown timer runs. When the reset_after cooldown of thirty seconds elapses, it transitions to HALF-OPEN and allows a single probe request. If the probe succeeds the breaker returns to CLOSED and the failure count resets; if the probe fails it returns to OPEN and the cooldown restarts. failures ≥ threshold (5) cooldown elapsed reset_after 30s probe fails probe succeeds success → reset count CLOSED pass through · count failures OPEN fast-fail · timer running HALF-OPEN allow a single probe

Troubleshooting

Symptom Likely cause Remediation
Legitimate units return no_match GTIN Company Prefix maps to the wrong responder Rebuild the routing table from the current GS1 prefix registry; verify longest-prefix order
Intermittent router_unavailable bursts Circuit breaker tripping on a flapping endpoint Inspect responder latency; raise threshold or reset_after, alert the partner
Duplicate verification charges Idempotency key missing or not honored on retry Require the idempotency header at ingress; cache decisions keyed on it
Responses slow under load Unbounded fan-out exhausting the connection pool Add an asyncio.Semaphore; size the pool per responder
Audit log has gaps Decision written after the response is returned Emit the audit record synchronously before returning to the requestor
Verification fails after a cert rotation Stale X.509 material in the responder cache Trigger a signed routing-table refresh; confirm the new chain validates

Frequently Asked Questions

What is a Verification Router Service in DSCSA? It is a stateless intermediary that receives a saleable-unit verification request keyed on the GTIN (01), serial (21), lot (10), and expiration (17), resolves the originating manufacturer’s endpoint from the GTIN Company Prefix, forwards the query, and returns a deterministic verified decision with a standardized reason code such as no_match, expired, or recalled.

Why route to the manufacturer instead of a central database? DSCSA verification is authoritative only at the source: only the originating manufacturer knows whether a specific serial was commissioned, dispensed, or recalled. A router preserves that source-of-truth model while giving requestors a single, standardized integration point instead of one bespoke connection per manufacturer.

How does the router isolate a failing manufacturer endpoint? A per-endpoint circuit breaker trips after a configured number of consecutive failures, fast-failing further requests to that responder for a cooldown window before allowing a single probe. This prevents one slow or down endpoint from exhausting the shared connection pool and degrading verification for every other partner.

What must be logged for a verification request? An append-only, hash-chained record of the request hash, resolved endpoint, verified outcome, reason code, latency, and correlation ID — retained for six years, cryptographically signed for non-repudiation, and reproducible for FDA inspection under 21 CFR Part 11.

How is a no_match different from a routing failure? A no_match is an authoritative answer from the manufacturer that the identifier is unknown and must be escalated as suspect product. A routing failure (router_unavailable) means the request never reached a responder; it must be retried or dead-lettered and must never be presented to the requestor as a suspect-product signal.

Conclusion

A production-grade Verification Router Service is less about networking and more about deterministic decisions under regulatory constraint. By modeling the verification key as a typed contract, resolving responders with longest-prefix matching against a signed routing table, isolating failures with circuit breakers, and treating every decision as an immutable, signed audit artifact, pharmaceutical organizations achieve near-real-time saleable-unit verification that stands up to inspection. The router remains compliant and operationally resilient as trading-partner networks, product portfolios, and DSCSA interoperability requirements evolve.