Skip to content
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Releases are also tagged in git, if that's helpful.
The following changes are not yet released, but are code complete:

Features:
-
- Add Trademark Trial and Appeal Board scraper #1851

Changes:
-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
"mspb_p",
"mspb_u",
"olc",
"ttab",
]
88 changes: 88 additions & 0 deletions juriscraper/opinions/united_states/administrative_agency/ttab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Scraper for Trademark Trial and Appeal Board (TTAB) Reading Room
CourtID: ttab
Court Short Name: TTAB
Author: Ansel Halliburton
Type: Precedential
"""

from datetime import date, datetime
from urllib.parse import urljoin

from juriscraper.AbstractSite import logger
from juriscraper.lib.string_utils import titlecase
from juriscraper.OpinionSiteLinear import OpinionSiteLinear


class Site(OpinionSiteLinear):
first_opinion_date = datetime(1986, 12, 23)
days_interval = 30
TTAB_RR_BASE = "https://ttab-reading-room.uspto.gov"
TTAB_RR_API = urljoin(TTAB_RR_BASE, "ttab-efoia-api/decision/search")
TTAB_RR_PDF_BASE = urljoin(TTAB_RR_BASE, "/cms/rest/")

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.court_id = self.__module__
self.url = self.TTAB_RR_API
self.method = "POST"
self.status = "Published"
self.should_have_results = True
self._search_payload = {
"dateRangeData": {},
"facetData": {},
"parameterData": {"precedentCitableIndicator": "Y"},
"searchText": "",
"sortDataBag": [{"issueDate": "desc"}],
"recordStartNumber": 0,
}
self.parameters = {}
self.request["parameters"] = {"json": self._search_payload}
self.make_backscrape_iterable(kwargs)

def _process_html(self):
results = self.html.get("results", [])
seen_docs = set()
for r in results:
doc_id = r.get("documentId", "")
if not doc_id:
logger.warning("Skipping result: no doc_id")
continue
if doc_id in seen_docs:
logger.warning(f"Skipping result: duplicate doc_id = {doc_id}")
continue
seen_docs.add(doc_id)
self.cases.append(
{
"name": r.get("partyName", "").strip(),
"url": urljoin(self.TTAB_RR_PDF_BASE, doc_id.lstrip("/")),
"date": r.get("issueDateStr", ""),
"docket": r.get("proceedingNumberDisplay", ""),
"judge": ", ".join(
map(titlecase, r.get("panelMember", "").split(";"))
), # "LYKOS;ENGLISH;COHEN" -> "Lykos, English, Cohen"
"author": titlecase(r.get("decisionWriter", "")),
"disposition": r.get("decision", ""),
"summary": r.get("issue", ""),
}
)

async def _download_backwards(self, dates: tuple[date, date]) -> None:
logger.info("Backscraping for range %s %s", *dates)
self._search_payload["dateRangeData"] = {
"decisionDate": {
"from": dates[0].strftime("%Y-%m-%d"),
"to": dates[1].strftime("%Y-%m-%d"),
}
}
self._search_payload["recordStartNumber"] = 0
self.html = await self._download()
total = self.html.get("recordTotalQuantity", 0)
self._process_html()

page_size = 25
start = page_size
while start < total:
self._search_payload["recordStartNumber"] = start
self.html = await self._download()
self._process_html()
start += page_size
30 changes: 30 additions & 0 deletions tests/examples/opinions/united_states/ttab_example.compare.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"case_dates": "2026-01-27",
"case_names": "August Storck KG v. Florend Ind\u00fastria e Com\u00e9rcio de Chocolates LTDA",
"download_urls": "https://ttab-reading-room.uspto.gov/cms/rest/legal-proceeding/91277224/decision/OPP_40.pdf",
"precedential_statuses": "Published",
"blocked_statuses": false,
"date_filed_is_approximate": false,
"dispositions": "Opposition Dismissed (based on registration for MERCI mark)",
"docket_numbers": "91277224",
"judges": "Lykos, English, Cohen",
"authors": "Cohen",
"case_name_shorts": "",
"summaries": "2(d)"
},
{
"case_dates": "2025-11-24",
"case_names": "CandyVerse, LLC v. Zeeth Ltd.",
"download_urls": "https://ttab-reading-room.uspto.gov/cms/rest/legal-proceeding/91289595/decision/OPP_27.pdf",
"precedential_statuses": "Published",
"blocked_statuses": false,
"date_filed_is_approximate": false,
"dispositions": "Final Decision on SJ: Opposition Dismissed",
"docket_numbers": "91289595",
"judges": "English, Cohen, Casagrande",
"authors": "Vigil",
"case_name_shorts": "",
"summaries": "2(d)"
}
]
75 changes: 75 additions & 0 deletions tests/examples/opinions/united_states/ttab_example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"recordTotalQuantity": 2,
"results": [
{
"_class": "gov.uspto.trademark.efoia.model.opensearch.TrademarkAppealDecision",
"id": "15518",
"panelMember": "LYKOS;ENGLISH;COHEN",
"panelId": 47006,
"issue": "2(d)",
"proceedingNumber": 91277224,
"proceedingNumberDisplay": "91277224",
"appealDecisionId": 15518,
"issueDate": 1769490000000,
"precedentCitableIndicator": "Y",
"opposerMarkGoodService": "MERCI \n(and variations including THANK YOU MEANS MERCI)\n\n(English translation: \u201cTHANK YOU\u201d) \n\n[Inter alia, chocolate, in Class 30] \n\nMERCI (and variations including DANKE HEI\u00dfT MERCI)\n\n(Claim of common law rights in chocolate bars and candies)",
"partyName": "August Storck KG v. Florend Ind\u00fastria e Com\u00e9rcio de Chocolates LTDA",
"proceedingTypeCd": "OPP",
"applicantMarkGoodService": " \n(with color claim) \n(English translation: \u201cTHANK YOU\u201d) \n\n[Chocolate; Chocolate bars, in Class 30]",
"decision": "Opposition Dismissed (based on registration for MERCI mark)",
"documentImageId": "26027ESWM00029A",
"decisionTypeCd": "F",
"documentId": "/legal-proceeding/91277224/decision/OPP_40.pdf",
"issueDateStr": "27-JAN-2026",
"indexComplete": "ic",
"decisionIssues": "5, 6f",
"decisionWriter": "COHEN",
"decisionTypeCategory": "Final Decision",
"groundsForRefusalCategory": [
"Likelihood of Confusion"
],
"issueTypeCategory": [
"TTAB General \ufffd Additional Claims, Issues, Procedures"
],
"issueTypeSubCategory": [
"Doctrine of Foreign Equivalents"
]
},
{
"_class": "gov.uspto.trademark.efoia.model.opensearch.TrademarkAppealDecision",
"id": "22068",
"panelMember": "ENGLISH;COHEN;CASAGRANDE",
"panelId": 46130,
"issue": "2(d) ",
"proceedingNumber": 91289595,
"proceedingNumberDisplay": "91289595",
"appealDecisionId": 22068,
"issueDate": 1763960400000,
"precedentCitableIndicator": "Y",
"opposerMarkGoodService": "CANDYVERSE \n\nand\n \n[Pending applications for, inter alia, candy, chocolate and confectionary products, in Class 30; retail store services featuring, inter alia, candy, ice cream, clothing, and gift items, in Class 35; educational and entertainment programs about candy and confections, in Class 41]\n(Common law claim of rights in both marks for same goods and services)",
"partyName": "CandyVerse, LLC v. Zeeth Ltd.",
"proceedingTypeCd": "OPP",
"applicantMarkGoodService": "CANDYVERSE\n[Inter alia, confectionery made of sugar, dessert puddings and bakery desserts, in Class 30; \nflavoured frozen soft drinks; iced soft drinks; part frozen slush fruit drinks; slush fruit drinks, in Class 32; retail store services featuring food and drink products, in Class 35]\n",
"decision": "Final Decision on SJ: Opposition Dismissed",
"documentImageId": "25328ESWM000125",
"decisionTypeCd": "M",
"documentId": "/legal-proceeding/91289595/decision/OPP_27.pdf",
"issueDateStr": "24-NOV-2025",
"indexComplete": "ic",
"decisionIssues": "5j, 8b, 13c\n",
"decisionWriter": "VIGIL",
"decisionTypeCategory": "Decision on Motion",
"motionTypeCategory": [
"Motion for Summary Judgment"
],
"groundsForRefusalCategory": [
"Likelihood of Confusion",
"Requirements"
],
"groundsForRefusalSubCategory": [
"Priority of use",
"Section 44(d) / 44(e) Requirements"
]
}
]
}
Loading