USPTO Data Schema Mapping: Implementation Guide for Automated Docketing
USPTO prosecution data arrives as loosely-typed JSON whose field names, event codes, and date semantics differ across application generations, yet a single mis-mapped actionCode or a timezone-naive mailDate can silently drop a statutory deadline and expose a firm to malpractice. This guide closes the gap between the raw Open Data Portal payload and the calendar-adjusted, statute-anchored due date a docketing system must emit — the deterministic translation layer that converts federal prosecution events into structured records without human reconciliation.
The design treats mapping as four separable, independently testable stages — schema-validated ingestion, deterministic event normalization, statute-anchored deadline resolution, and immutable audit logging. It sits directly beneath the broader Core Docketing Architecture & Deadline Taxonomy, consumes the migration-specific field differences documented in USPTO PAIR vs Patent Center Data Structures, and hands its normalized events to the shared Automated Deadline Calculation & Rule Engines layer for final due-date arithmetic.
Compliance & Scope Boundaries
The USPTO Open Data Portal is a public, terms-bound service, and the mapping layer must operate strictly inside its published envelope. Several boundaries are non-negotiable and belong in code review before anything ships:
- Respect the published API quotas. The Open Data Portal enforces per-key rate limits and returns
429with aRetry-Afterheader rather than data once you exceed them. Throughput shaping is out of scope here and belongs in the ingestion layer described under USPTO Patent Center Web Scraping; this pipeline assumes a token bucket is already fronting every call. - Query the API, do not scrape the human UI, while the API is healthy. The REST interface is the sanctioned machine-readable channel. Automated retrieval from the Patent Center web front-end is a last-resort fallback and must honor its
robots.txt. - Computation is advisory, never authoritative. Every emitted date is decision-support. The controlling deadline is whatever the USPTO recognizes on the record; each output must be traceable to the exact event, rule version, and closure calendar that produced it.
- Data minimization and access control. Extract only the fields required for deadline calculation. Inventor addresses, attorney contact details, and unpublished application content are sensitive and must be stripped or gated per the Security & Access Control Boundaries policy before payloads enter analytics or reminder pipelines.
Prerequisites & Dependency Map
The mapping worker has a small, explicit dependency surface. Pin every item so a behavioral change is a reviewable diff rather than ambient drift.
| Dependency | Minimum version | Role |
|---|---|---|
| Python | 3.11 | Native zoneinfo, datetime.UTC, structural pattern matching |
httpx |
0.27 | HTTP/2 client for authenticated Open Data Portal fetches |
pydantic |
2.5 | Payload validation and field coercion |
tenacity |
8.2 | Declarative retry/backoff on transient network faults |
python-dateutil |
2.8 | relativedelta calendar-month arithmetic |
tzdata |
2024.1+ | IANA zone database on platforms without a system copy |
Upstream inputs that must be resolved before the worker runs:
- Open Data Portal API key — issued from the USPTO Developer Hub, injected via a secrets manager, never hardcoded.
- Application number — normalized to the USPTO series/serial form (
17/123,456→17123456) before any query. - Event rule file — a version-pinned mapping of
actionCode→ canonical event class and deadline policy, cited to 35 U.S.C. and 37 CFR. - USPTO closure calendar — the federal holidays and DC-specific closure days the Office observes, pinned to a release tag, used for the 37 CFR § 1.7 roll-forward.
# uspto_event_rules.yaml
# Source of truth: 35 U.S.C. (§§ 41, 133, 151) + 37 CFR (§§ 1.7, 1.136, 1.311).
# https://www.uspto.gov/web/offices/pac/mpep/ and https://developer.uspto.gov/
rule_version: "2026.07.0"
# USPTO action codes -> canonical event class.
event_codes:
CTNF: NON_FINAL_OA # non-final Office action; opens response window
CTFR: FINAL_OA # final Office action; opens response + RCE window
MN/=.: NOTICE_OF_ALLOW # Notice of Allowance; opens issue-fee window
N271: ISSUE_NOTIFICATION # issue notification; patent grant imminent
ABN: ABANDONMENT # abandonment; terminal state, close open deadlines
deadlines:
NON_FINAL_OA: # 35 U.S.C. § 133; 37 CFR § 1.136(a)
anchor: mail_date
months: 3
extendable: true # extendable to 6 months total under 37 CFR 1.136(a)
max_months: 6
shift: following # 37 CFR 1.7: roll forward off weekends/closures
NOTICE_OF_ALLOW: # 35 U.S.C. § 151; 37 CFR § 1.311
anchor: mail_date
months: 3
extendable: false # issue-fee window is NOT extendable
shift: following
Step-by-Step Implementation
The worker is a deterministic pipeline anchored to a single application. Each step below is independently verifiable — run its snippet in isolation and assert the intermediate value before composing the whole.
Step 1 — Fetch prosecution events with retry-safe transport
Query the Open Data Portal transactions endpoint for one application, and wrap the call in tenacity so transient timeouts and 5xx faults back off instead of failing the batch. Send the API key as a header; never place it in the query string, where it lands in access logs.
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
ODP_BASE = "https://api.uspto.gov/api/v1/patent/applications"
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((httpx.TimeoutException, httpx.HTTPStatusError)),
)
def fetch_prosecution_events(app_number: str, api_key: str) -> dict:
"""Fetch the raw transaction/event payload for one application."""
normalized_id = app_number.replace("/", "").replace(",", "").strip()
url = f"{ODP_BASE}/{normalized_id}/transactions"
headers = {"Accept": "application/json", "X-API-KEY": api_key}
resp = httpx.get(url, headers=headers, timeout=15.0)
resp.raise_for_status()
return resp.json()
# Verify: a known granted application returns a non-empty events list.
# assert fetch_prosecution_events("17/123,456", KEY).get("events")
Step 2 — Validate each event against a strict contract
Every raw record passes through a Pydantic v2 model before any arithmetic touches it. Validation failures are logged with the offending application number and skipped, so a single malformed record never halts the batch. Dates are coerced to timezone-aware values and future-dated events are rejected as sync errors.
import logging
from datetime import date, datetime
from typing import Literal
from pydantic import BaseModel, Field, ValidationError, field_validator, ConfigDict
logger = logging.getLogger(__name__)
class USPTOEvent(BaseModel):
model_config = ConfigDict(populate_by_name=True)
application_number: str = Field(alias="applNum", pattern=r"^\d{2}/?\d{3},?\d{3}$")
event_code: str = Field(alias="actionCode")
mail_date: date = Field(alias="mailDate")
status: Literal["ACTIVE", "ABANDONED", "PATENTED", "WITHDRAWN"] = Field(alias="status")
@field_validator("mail_date", mode="before")
@classmethod
def normalize_date(cls, v: str | date) -> date:
# USPTO emits ISO 8601 UTC ("2026-03-11T00:00:00Z"); take the calendar day.
if isinstance(v, str):
return datetime.fromisoformat(v.replace("Z", "+00:00")).date()
return v
@field_validator("mail_date")
@classmethod
def reject_future_dates(cls, v: date) -> date:
if v > date.today():
raise ValueError("mailDate cannot be in the future — likely a sync error")
return v
Step 3 — Normalize action codes to a canonical taxonomy
Raw USPTO payloads carry hundreds of actionCode values. Map each one through the version-pinned rule file into a single canonical class, and fail closed on any code the file does not recognize — a silent default is a malpractice vector because it means an event that may open a deadline is being dropped.
def normalize_events(events: list[USPTOEvent], rules: dict) -> list[dict]:
"""Flatten validated events into canonical {class, code, date} records.
Unmapped codes are surfaced, not swallowed: an unknown code usually means
the rule file lags a USPTO taxonomy change and needs human confirmation.
"""
code_map: dict[str, str] = rules["event_codes"]
out: list[dict] = []
for ev in events:
canonical = code_map.get(ev.event_code)
if canonical is None:
raise ValueError(f"Unmapped USPTO actionCode: {ev.event_code!r} — verify rule file")
out.append({
"event_class": canonical,
"event_code": ev.event_code,
"event_date": ev.mail_date,
})
return out
Step 4 — Resolve statutory deadlines deterministically
The payload is the source of truth for status; the rule file governs calculation. Apply the statutory window (three months for a non-final Office action response under 35 U.S.C. § 133; three months for the issue-fee window under 35 U.S.C. § 151) and roll any date that lands on a weekend or a USPTO closure day forward under 37 CFR § 1.7. Anchor time to USPTO local time so a UTC midnight boundary never shifts the calendar day.
from datetime import date, timedelta
from zoneinfo import ZoneInfo
from dateutil.relativedelta import relativedelta
USPTO_TZ = ZoneInfo("America/New_York") # USPTO operates on Eastern Time.
def shift_off_closures(target: date, closures: frozenset[date]) -> tuple[date, bool]:
"""Roll forward past weekends and USPTO closure days (37 CFR 1.7)."""
adjusted = False
while target.weekday() >= 5 or target in closures: # 5=Sat, 6=Sun
target += timedelta(days=1)
adjusted = True
return target, adjusted
def resolve_deadline(
rule_key: str, anchor: date, rules: dict, closures: frozenset[date]
) -> tuple[date, bool]:
spec = rules["deadlines"][rule_key]
raw = anchor + relativedelta(months=int(spec["months"]))
return shift_off_closures(raw, closures)
# Verify: a 3-month issue-fee window off a Wednesday mail date, no closures.
# NOA mailed 2025-06-11 (Wed) + 3 months = 2025-09-11 (Thu) -> no shift.
Step 5 — Emit each event with an immutable audit record
The output is never a bare date. It carries the applied rule key, the closure-shift flag, the rule version, and a SHA-256 hash of the exact inputs, so a compliance dashboard can reconstruct precisely which prosecution event and rule produced it — the same discipline enforced across the parent taxonomy.
import hashlib
def build_audit_hash(app_number: str, code: str, deadline: date, rule_version: str) -> str:
payload = f"{app_number}|{code}|{deadline.isoformat()}|{rule_version}"
return hashlib.sha256(payload.encode()).hexdigest()
API Contract & Schema
Docketing platforms consume this worker through a stateless, idempotent boundary. Strict Pydantic v2 validation rejects malformed data before any arithmetic, and an idempotency key deduplicates events across overlapping polls so an API retry never generates a duplicate docket entry.
from datetime import date
from typing import Literal
from pydantic import BaseModel, Field
CanonicalClass = Literal[
"NON_FINAL_OA", "FINAL_OA", "NOTICE_OF_ALLOW", "ISSUE_NOTIFICATION", "ABANDONMENT"
]
class CanonicalUsptoEvent(BaseModel):
application_number: str = Field(pattern=r"^\d{8}$") # series+serial, no separators
event_code: str = Field(pattern=r"^[A-Z0-9/=.]{2,6}$")
event_class: CanonicalClass
event_date: date
@property
def idempotency_key(self) -> str:
# Same event seen on two polls collapses to one docket entry.
return f"{self.application_number}:{self.event_code}:{self.event_date.isoformat()}"
class ResolvedDeadline(BaseModel):
application_number: str
rule_key: str
deadline: date
closure_adjusted: bool
rule_version: str
audit_hash: str
compliance_status: Literal["ACTIVE", "REVIEW_REQUIRED"] = "ACTIVE"
A caller replaying the same idempotency_key receives the identical resolved deadline without re-triggering downstream reminder webhooks. The construction of the audit hash from application_number + event_code + event_date — not from a server-assigned row id — is what makes deduplication stable across pipeline restarts.
Edge Cases & Failure Modes
The happy path is trivial; the value of this worker is in the failures it refuses to hide.
- Extendable versus non-extendable windows. A non-final Office action response runs three months but is extendable to six under 37 CFR § 1.136(a); the issue-fee window under 35 U.S.C. § 151 is not extendable. The engine must never apply an extension buffer to
NOTICE_OF_ALLOW, and any override attempt should be rejected in code review. - Closure collision after the weekend shift. Rolling off a Saturday can land on a Monday that is itself a federal holiday. The shift loop must re-test the condition (hence the
whilein Step 4), not shift a single day. - Terminal status overrides pending actions. An
ABANDONMENTorPATENTEDstatus must close all open deadlines for that application. If the mapping layer emits a response deadline for an abandoned case, it generates a false reminder and erodes trust in the calendar. - UTC midnight day drift. A
mailDateserialized as2026-03-11T00:00:00Zis 8 March 7 PM the previous day on the US West Coast if parsed naively into local time. Take the calendar day from the UTC value (Step 2) and anchor arithmetic to USPTO Eastern Time, never to the server’s local zone. - Unknown or deprecated action code. A new
actionCodeappearing after a USPTO taxonomy update must halt normalization for that record rather than default to a benign class. This is the schema-drift failure mode covered in depth by Schema Validation & Error Categorization; pinrule_versionand alert on any code the file does not map. - Rate-limit rejection versus outage. A
429withRetry-Aftermeans quota exhaustion (back off and defer); a5xxor connection error means an Open Data Portal outage (retry with jitter, then fall back to cached last-known state). Conflating the two either burns quota or silently stalls docketing.
Verification & Regression Testing
Anchor the worker to known-good dates and run the suite on every rule-file change. These assertions are the contract:
from datetime import date
RULES = {
"rule_version": "2026.07.0",
"event_codes": {"CTNF": "NON_FINAL_OA", "MN/=.": "NOTICE_OF_ALLOW", "ABN": "ABANDONMENT"},
"deadlines": {
"NON_FINAL_OA": {"months": 3, "shift": "following"},
"NOTICE_OF_ALLOW": {"months": 3, "shift": "following"},
},
}
def test_issue_fee_window_no_shift():
# NOA mailed 2025-06-11 (Wed) + 3 months = 2025-09-11 (Thu) -> no shift.
deadline, adjusted = resolve_deadline("NOTICE_OF_ALLOW", date(2025, 6, 11), RULES, frozenset())
assert deadline == date(2025, 9, 11)
assert adjusted is False
def test_response_deadline_rolls_off_closure():
# OA mailed 2025-01-04 + 3 months = 2025-04-04 (Fri). Treat it as a USPTO
# closure day; the deadline must roll forward to Monday 2025-04-07.
deadline, adjusted = resolve_deadline(
"NON_FINAL_OA", date(2025, 1, 4), RULES, frozenset({date(2025, 4, 4)})
)
assert deadline == date(2025, 4, 7)
assert adjusted is True
def test_unmapped_code_halts():
events = [USPTOEvent.model_validate(
{"applNum": "17/123,456", "actionCode": "ZZ99", "mailDate": "2025-06-11", "status": "ACTIVE"}
)]
try:
normalize_events(events, RULES)
assert False, "expected ValueError for unmapped code"
except ValueError:
pass
The issue-fee case pins the three-month arithmetic, the response-deadline case proves the 37 CFR § 1.7 closure roll-forward fires (and correctly skips the weekend to land on Monday), and the unmapped-code case proves the worker fails closed rather than guessing.
Operational Action Summary
Operational Action: Treat uspto_event_rules.yaml as code — gate it through peer review by patent counsel, pin rule_version, and log every computation (raw event, applied rule, shift flag, output, audit hash) to append-only storage. Route any REVIEW_REQUIRED record to a paralegal before emission.
Operational Action: Inject the Open Data Portal API key from a secrets manager and never commit it or place it in a query string; scope tokens to read-only endpoints and align credential handling with the Security & Access Control Boundaries policy.
Operational Action: Distinguish 429 (quota — back off on Retry-After) from 5xx (outage — retry with jitter, then fall back to cached last-known state with a stale_data flag), and enforce a circuit breaker that halts automated docket writes after three consecutive 5xx errors while keeping read-only UI fallbacks alive.
Frequently Asked Questions
How long is the response period after a non-final USPTO Office action?
mailDate under 35 U.S.C. § 133, extendable in one-month increments up to a maximum of six months by filing a petition and fee under 37 CFR § 1.136(a). The rule file marks it extendable: true with max_months: 6.
Is the issue-fee deadline after a Notice of Allowance extendable?
NOTICE_OF_ALLOW as extendable: false, and the resolver applies only the 37 CFR § 1.7 closure shift, never a discretionary buffer.
What happens if a USPTO deadline falls on a weekend or federal holiday?
shift_off_closures helper rolls the raw statutory date forward past weekends and the pinned USPTO closure calendar, setting closure_adjusted when it does.
How do I stop an API retry from creating duplicate docket entries?
application_number + event_code + event_date — rather than from a server row id, and deduplicate on it before writing. A replayed event then collapses to the same docket entry and does not re-fire reminder webhooks.
What should the pipeline do when it sees an unknown actionCode?
actionCode usually means the USPTO taxonomy changed and the pinned rule file lags it. Normalization raises rather than defaulting to a benign class, the record is routed to review, and an alert fires — so a code that may open a deadline is never silently dropped.
Related
- USPTO PAIR vs Patent Center Data Structures — the field-level serialization and event-taxonomy differences this mapping layer must absorb.
- USPTO Patent Center vs EPO Register Status Codes — mapping office event/legal-status codes onto one canonical status.
- EPO Register Sync Architecture — the parallel European pipeline; align field dictionaries when portfolios span both offices.
- PCT National Phase Entry Rules — reconcile WIPO priority dates with USPTO national-phase filing rules.
- WIPO PATENTSCOPE Integration — the international data source feeding cross-jurisdictional normalization.
- Automated Deadline Calculation & Rule Engines — the downstream engine that consumes these normalized events.
For authoritative references, practitioners should consult the USPTO Manual of Patent Examining Procedure for statutory day-counting and 37 CFR § 1.7 extension logic, and the USPTO Developer Hub for Open Data Portal endpoints and authentication. Python implementations should rely on the standard-library zoneinfo module and dateutil.relativedelta for calendar-correct arithmetic, and on the Pydantic V2 documentation for validation contracts.