WIPO PATENTSCOPE Integration: Implementation Guide for PCT Docket Automation
PATENTSCOPE is the authoritative source for PCT bibliographic data, priority chains, and international publication events, yet its payloads arrive as nested multi-jurisdictional XML/JSON whose priority arrays and legal-event codes must be mapped without loss before a single national-phase deadline can be trusted. This guide closes the gap between a raw PATENTSCOPE record and the calendar-adjusted, treaty-anchored due date a docketing system must emit — the deterministic ingestion layer that turns a manual research portal into a defensible, replayable feed.
The design treats integration as four separable, independently testable stages — authenticated retrieval, ST.3/ST.6 schema normalization, treaty-anchored deadline resolution, and immutable audit logging. It sits directly beneath the broader Core Docketing Architecture & Deadline Taxonomy, aligns its field dictionary with the USPTO Data Schema Mapping conventions when portfolios span both offices, and hands its computed windows to the PCT National Phase Entry Rules framework for per-office resolution. Throughput shaping and long-poll retrieval belong upstream in the WIPO API Async Polling Patterns layer; this pipeline assumes a rate-limited transport is already fronting every call.
X-API-Key header and backs off on 429/503, the record is fetched from the JSON gateway rather than the result.jsf HTML page, the payload is normalized against ST.3/ST.6 with the raw copy persisted for schema-drift detection, a version-pinned rule file and per-office closure calendars drive the 30/31-month deadline with a PCT Rule 80.5 roll off the national office calendar, and each result is emitted as a DocketEvent sealed with a SHA-256 audit record.Compliance & Scope Boundaries
PATENTSCOPE is a public WIPO service governed by published terms of use, and the ingestion layer must operate strictly inside that envelope. Several boundaries are non-negotiable and belong in code review before anything ships:
- Query the sanctioned machine-readable channel, not the human UI. The public
result.jsfsearch page returns HTML for interactive browsing and is not a supported programmatic surface; scraping it is brittle and against the spirit of the terms. Production ingestion routes through the authenticated JSON/SOAP web services provisioned via the WIPO developer portal, and honorsrobots.txtfor any fallback path. - Respect the published rate limits. WIPO enforces per-key quotas and returns
429 Too Many Requestsor503 Service Unavailablerather than data once you exceed them. Concurrency tuning and backoff are owned upstream by the WIPO API Async Polling Patterns layer; this pipeline assumes a token bucket already shapes throughput and never hammers the service to fill a batch. - Computation is advisory, never authoritative. Every emitted date is decision-support. The controlling deadline is whatever the national office of entry recognizes on the record; each output must trace back to the exact priority claim, treaty rule, and closure calendar that produced it.
- Data minimization and access control. Extract only the fields required for docketing. Inventor addresses and agent contact details are sensitive; strip or gate them per the Security & Access Control Boundaries policy before payloads enter analytics or reminder pipelines. Scope PATENTSCOPE keys to read-only service accounts.
Prerequisites & Dependency Map
The ingestion 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 PATENTSCOPE 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:
- PATENTSCOPE API key — provisioned through the WIPO developer portal, injected via a secrets manager, transmitted in an
X-API-Keyheader (or a WS-Security token for SOAP), never hardcoded or placed in a query string. - PCT publication number — normalized to canonical WIPO ST.6 form before any query.
- National-phase rule file — a version-pinned map of jurisdiction → national-phase window (months) and closure calendar, cited to the WIPO PCT Contracting States table.
- Per-office closure calendars — the days each national office of entry is closed for receipt of documents, used for the PCT Rule 80.5 roll-forward.
# pct_national_phase_rules.yaml
# Source of truth: PCT Articles 22 & 39; PCT Rule 80.5 (closure roll).
# Contracting-state windows: https://www.wipo.int/pct/en/pct_contracting_states.html
rule_version: "2026.07.0"
# Most offices allow 30 months from the earliest priority date; a minority
# (and the EPO regional route) use 31. Always verify the live WIPO table —
# national law can and does change these values.
default_months: 30
national_phase_months:
EP: 31 # EPO regional phase — PCT Art. 39 / EPC
JP: 30 # JPO — PCT Art. 22
US: 30 # USPTO — 35 U.S.C. 371(c); roll under 35 U.S.C. 21(b) / 37 CFR 1.7
AU: 31 # IP Australia
fee_grace_days: 0 # PCT itself grants no uniform grace; office-specific only
Step-by-Step Implementation
The worker is a deterministic pipeline anchored to a single PCT publication. Each step below is independently verifiable — run its snippet in isolation and assert the intermediate value before composing the whole.
Step 1 — Authenticate and fetch with retry-safe transport
Build a session that sends the API key as a header and wraps every call in tenacity, so transient timeouts, 5xx faults, and 429/503 backpressure back off instead of failing the batch. A circuit breaker upstream halts polling when rejections exceed a threshold; here we simply retry with exponential backoff and jitter.
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
PATENTSCOPE_BASE = "https://api.wipo.int/patentscope/v1"
@retry(
stop=stop_after_attempt(4),
wait=wait_exponential_jitter(initial=0.5, max=10),
retry=retry_if_exception_type((httpx.TimeoutException, httpx.HTTPStatusError)),
)
def fetch_patentscope_record(publication_id: str, api_key: str) -> dict:
"""Fetch one PCT bibliographic record from the authenticated JSON gateway.
Production deployments front PATENTSCOPE JSON behind an authenticated
gateway; the public result.jsf page returns HTML and is unsuitable for
programmatic ingestion.
"""
url = f"{PATENTSCOPE_BASE}/records/{publication_id}"
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 WO publication returns a payload carrying a priority array.
# assert fetch_patentscope_record("WO2023123456", KEY).get("priorityDates")
Attach a deterministic X-Request-ID to every outbound call so a reconciliation audit can replay it, and cache static bibliographic responses (24-hour TTL) with immediate invalidation whenever legalStatus, designatedStates, or the priority chain mutates.
Step 2 — Validate and normalize the record against a strict contract
Every raw record passes through a Pydantic v2 model before any arithmetic touches it. Publication and application numbers are checked against WIPO ST.3 (country codes) and ST.6 (kind codes); applicant names are canonicalized with UTF-8 NFC normalization so cross-office reporting is stable. Validation failures are logged with the offending publication number and skipped, so one malformed record never halts the batch.
import logging
import unicodedata
from datetime import date
from pydantic import BaseModel, field_validator
import re
logger = logging.getLogger(__name__)
# WIPO ST.6 WO publication: "WO" + 4-digit year + 6-digit serial + kind code
# (A1/A2/A3/B1...). The slash between year and serial is optional in canonical form.
_ST6_WO = re.compile(r"^WO\d{4}/?\d{6}\s?[A-Z]\d?$")
class PatentscopeBiblio(BaseModel):
publication_number: str
application_number: str
priority_dates: list[date]
legal_status: str
applicant_names: list[str]
@field_validator("publication_number")
@classmethod
def validate_st6_format(cls, v: str) -> str:
if not _ST6_WO.match(v):
raise ValueError(f"Invalid WIPO ST.6 publication format: {v!r}")
return v
@field_validator("applicant_names")
@classmethod
def normalize_names(cls, v: list[str]) -> list[str]:
# NFC folds composed/decomposed Unicode so "José" compares equal
# regardless of how PATENTSCOPE serialized the diacritic.
return [unicodedata.normalize("NFC", name.strip()) for name in v if name.strip()]
Persist the raw API response alongside the normalized record. Keeping both satisfies audit retention and lets a nightly job detect schema drift the moment WIPO changes a field shape — the same failure mode categorized by the Schema Validation & Error Categorization layer.
Step 3 — Resolve the national-phase deadline deterministically
National phase entry runs 30 months from the earliest priority date under PCT Article 22, and 31 months under Article 39 where a demand for international preliminary examination applies — though many offices set a longer uniform period by national law. Derive the deadline from the validated priority array, never from a cached or manually entered date, and roll any date landing on a day the national office of entry is closed under PCT Rule 80.5.
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta
def resolve_national_phase(
priority_dates: list[date],
jurisdiction: str,
rules: dict,
closures: frozenset[date],
) -> tuple[date, bool]:
"""Compute the national-phase entry deadline for one office (PCT Art. 22/39)."""
earliest = min(priority_dates) # the priority chain anchors everything
months = rules["national_phase_months"].get(
jurisdiction.upper(), rules["default_months"]
)
raw = earliest + relativedelta(months=months)
# PCT Rule 80.5: if the period expires on a day the RELEVANT office (the
# national office of entry, not WIPO) is closed, it rolls to the next day
# that office is open. Re-test in a loop so a Monday holiday after a
# weekend also rolls.
adjusted = False
while raw.weekday() >= 5 or raw in closures: # 5=Sat, 6=Sun
raw += timedelta(days=1)
adjusted = True
return raw, adjusted
# Verify: earliest priority 2022-03-15, US (30 months) -> 2024-09-15 (Sun)
# rolls forward to Monday 2024-09-16 under Rule 80.5 + the office calendar.
Per-office window selection and the finer points of Article 22 versus Article 39 are owned by the PCT National Phase Entry Rules framework; this worker consumes that rule file rather than hardcoding month counts.
Step 4 — Handle sequence listings and biological assets
Biotech and pharmaceutical portfolios include sequence listings governed by WIPO ST.25 (legacy TXT) and ST.26 (XML). PATENTSCOPE exposes these as downloadable attachments or embedded XML. Validate the listing against its schema and link each biological asset to the correct priority claim before it can influence downstream docketing.
from dataclasses import dataclass
@dataclass(frozen=True)
class SequenceListingRef:
publication_number: str
standard: str # "ST.26" (XML) or "ST.25" (legacy TXT)
attachment_url: str
linked_priority: date # bind the listing to a specific priority claim
def classify_sequence_listing(payload: dict) -> SequenceListingRef | None:
"""Detect and classify a sequence listing on a PATENTSCOPE record."""
seq = payload.get("sequenceListing")
if not seq:
return None
standard = "ST.26" if seq.get("format", "").lower() == "xml" else "ST.25"
return SequenceListingRef(
publication_number=payload["publicationNumber"],
standard=standard,
attachment_url=seq["href"],
linked_priority=min(date.fromisoformat(d) for d in payload["priorityDates"]),
)
Full XSD validation and INSDC-formatting checks are detailed in the WIPO Sequence Listing Format Parsing Guide; this step only detects and routes, so a malformed listing never silently corrupts a docket record.
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 priority claim and treaty rule produced it — the same discipline enforced across the parent taxonomy.
import hashlib
def build_audit_hash(pub_number: str, priority: date, deadline: date, rule_version: str) -> str:
payload = f"{pub_number}|{priority.isoformat()}|{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 a retry never generates a duplicate docket entry.
from datetime import date
from typing import Literal
from pydantic import BaseModel, Field
class PctDocketEvent(BaseModel):
publication_number: str = Field(pattern=r"^WO\d{4}/?\d{6}\s?[A-Z]\d?$")
jurisdiction: str = Field(pattern=r"^[A-Z]{2}$") # ST.3 country code
earliest_priority: date
@property
def idempotency_key(self) -> str:
# Same publication + office seen on two polls collapses to one entry.
return f"{self.publication_number}:{self.jurisdiction}:{self.earliest_priority.isoformat()}"
class ResolvedPctDeadline(BaseModel):
publication_number: str
jurisdiction: str
national_phase_entry: 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. Deriving the key from publication_number + jurisdiction + earliest_priority — not from a server-assigned row id — is what keeps deduplication stable across pipeline restarts and re-syncs.
Edge Cases & Failure Modes
The happy path is trivial; the value of this worker is in the failures it refuses to hide.
- The “relevant office” is national, not WIPO. PCT Rule 80.5 rolls a period expiring on a closure day of the national office of entry, not WIPO’s Geneva calendar. The same 30-month anchor therefore produces different effective dates in different countries. Never apply a single global holiday calendar to a multi-jurisdiction batch.
- Earliest priority, not filing date. The window runs from the earliest priority date in the chain, not the international filing date. A record with multiple priority claims must resolve
min(priority_dates); picking the filing date silently grants weeks of phantom runway. - Article 22 versus Article 39 (30 vs 31 months). Filing a demand for international preliminary examination shifts many offices to 31 months, but a growing number set a uniform national period regardless. Read the window from the version-pinned rule file, and re-verify against the live WIPO Contracting States table on each rule release.
- Closure collision after the weekend shift. Rolling off a Saturday can land on a Monday the office is itself closed. The shift loop must re-test the condition (hence the
whilein Step 3), not shift a single day. - ST.6 kind-code variants. WO publications carry kind codes
A1/A2/A3/B1and republications; a regex pinned to a single suffix rejects valid records. Anchor validation to the ST.6 grammar and alert — do not silently drop — on any publication the pattern fails. - Schema drift and deprecated fields. A field WIPO renames or nests differently after an API update must halt normalization for that record rather than yield a null. Persist the raw payload (Step 2), pin the schema, and route drift to review — the failure mode owned by Schema Validation & Error Categorization.
- Rate-limit rejection versus outage. A
429/503withRetry-Aftermeans quota exhaustion or maintenance (back off and defer); a bare5xxor connection error means a transient fault (retry with jitter, then fall back to cached last-known state). Conflating them 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",
"default_months": 30,
"national_phase_months": {"EP": 31, "US": 30},
}
def test_us_30_month_from_earliest_priority():
# Two priority claims; the EARLIEST anchors the window.
# 2022-01-10 + 30 months = 2024-07-10 (Wed) -> no shift.
deadline, adjusted = resolve_national_phase(
[date(2022, 1, 10), date(2022, 6, 1)], "US", RULES, frozenset()
)
assert deadline == date(2024, 7, 10)
assert adjusted is False
def test_epo_31_month_window():
# EPO regional phase uses 31 months under PCT Art. 39.
deadline, _ = resolve_national_phase([date(2022, 1, 10)], "EP", RULES, frozenset())
assert deadline == date(2024, 8, 10)
def test_rule_80_5_rolls_off_office_closure():
# 2022-03-15 + 30 months = 2024-09-15 (Sun) -> rolls to Mon 2024-09-16.
deadline, adjusted = resolve_national_phase(
[date(2022, 3, 15)], "US", RULES, frozenset()
)
assert deadline == date(2024, 9, 16)
assert adjusted is True
def test_st6_validation_rejects_malformed():
import pytest
from pydantic import ValidationError
with pytest.raises(ValidationError):
PatentscopeBiblio(
publication_number="US2023123456A1", # not a WO publication
application_number="PCT/US2022/012345",
priority_dates=[date(2022, 1, 10)],
legal_status="PENDING",
applicant_names=["Acme Corp"],
)
The first case pins the 30-month arithmetic and proves the earliest priority anchors it; the EPO case proves the 31-month branch; the Rule 80.5 case proves the closure roll-forward fires and skips the weekend; and the ST.6 case proves the worker rejects a non-WO publication rather than mis-docketing it.
Operational Action Summary
Operational Action: Treat pct_national_phase_rules.yaml as code — gate it through peer review by patent counsel, pin rule_version, and re-validate every window against the live WIPO Contracting States table on each release. Log every computation (raw record hash, priority chain, applied rule, shift flag, output, audit hash) to append-only storage, and route any REVIEW_REQUIRED record to a paralegal before emission.
Operational Action: Inject the PATENTSCOPE API key from a secrets manager, scope it to read-only endpoints, transmit it only in the X-API-Key header, rotate it on a fixed cadence, and align credential handling with the Security & Access Control Boundaries policy.
Operational Action: Distinguish 429/503 (quota or maintenance — back off on Retry-After) from bare 5xx/connection faults (transient — 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 repeated failures while keeping read-only fallbacks alive. Emit patentscope.api.success_rate, patentscope.schema.validation_failures, and patentscope.deadline.calculation_latency, and schedule nightly reconciliation against fresh queries to flag priority-chain or legal-status drift.
Frequently Asked Questions
What happens if the 30-month PCT national-phase deadline falls on a weekend?
resolve_national_phase helper re-tests the condition in a loop, so a deadline landing on a Sunday before a Monday holiday rolls forward past both, setting closure_adjusted.
Is the national-phase window measured from the filing date or the priority date?
min(priority_dates); anchoring to the filing date silently grants weeks of phantom runway and is a malpractice vector.
When is the window 30 months versus 31 months?
Should I scrape the PATENTSCOPE result.jsf search page?
result.jsf page returns HTML for interactive browsing and is not a supported programmatic surface; scraping it is brittle and against the terms of use. Retrieve records through the authenticated JSON/SOAP web services provisioned from the WIPO developer portal, send the key in an X-API-Key header, and let the async polling layer shape throughput within WIPO's published rate limits.
How do I stop a re-sync from creating duplicate PCT docket entries?
publication_number + jurisdiction + earliest_priority — rather than from a server row id, and deduplicate on it before writing. A record seen on two overlapping polls then collapses to the same docket entry and does not re-fire reminder webhooks.
Related
- WIPO Sequence Listing Format Parsing Guide — ST.25/ST.26 XSD validation and INSDC checks for biological assets on PATENTSCOPE records.
- PCT National Phase Entry Rules — the Article 22/39 window selection and per-office rule file this worker consumes.
- USPTO Data Schema Mapping — align field dictionaries when a portfolio spans PCT and US domestic prosecution.
- WIPO API Async Polling Patterns — the upstream transport that shapes throughput and handles long-poll retrieval within WIPO’s rate limits.
- Security & Access Control Boundaries — read-only key scoping, credential rotation, and PII minimization for international data.
For authoritative references, practitioners should consult the WIPO PCT Contracting States table for current national-phase windows and the PCT Applicant’s Guide for Article 22/39 and Rule 80.5 day-counting. 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.