A Go client for the USPTO Open Data Portal (ODP) REST API, the Office Action APIs (DSAPI), and TSDR (Trademark Status & Document Retrieval).
- Patent Application API (13 endpoints) - search, metadata, continuity, assignment, adjustment, attorney, foreign priority, transactions, documents.
- Bulk Data API (3 endpoints) - product search and resumable file downloads with progress callbacks.
- Petition API (3 endpoints) - petition decision search, retrieval, download.
- PTAB API (19 endpoints) - trial proceedings, decisions, documents, appeal decisions, interference decisions.
- Office Action APIs (8 endpoints) - text retrieval, citations, rejections, enriched citations, via the DSAPI Lucene/Solr pattern.
- TSDR (7 wrapper methods) - trademark status and document retrieval on a separate server with its own key.
- Patent number normalization - accepts application, grant, publication, and PCT formats; resolves grant/publication numbers to application numbers.
- XML full text - parses ICE DTD 4.6/4.7 grant and application documents.
- Retry logic with exponential backoff,
Retry-Afterhonoring, and configurable timeouts.
USPTO bumps the portal version (currently 4.0) for any change to the
ODP -- UI updates, new bulk datasets, ecosystem additions -- even when the
underlying API specs don't change. Each swagger file carries its own
info.version, and that's the contract this client binds to.
| Spec file | Version | Surface |
|---|---|---|
swagger.yaml |
1.0.0 | Patent Application, Bulk Data, Petition, PTAB |
odp-common-base.yaml |
1.0.0 | Shared schemas |
trial-proceedings.yaml |
1.0.0 | PTAB trial proceedings |
trial-decisions.yaml |
1.0.0 | PTAB trial decisions |
trial-documents.yaml |
1.0.0 | PTAB trial documents |
trial-appeal-decisions.yaml |
1.0.0 | PTAB appeal decisions |
trial-interferences.yaml |
1.0.0 | PTAB interference decisions |
trial-common.yaml |
1.1.0 | Shared PTAB types |
oa-text-retrieval.yaml |
1.0.0 | Office Action text |
oa-citations.yaml |
1.0.0 | Office Action citations |
oa-rejections.yaml |
1.0.0 | Office Action rejections |
oa-enriched-citations.yaml |
1.0.0 | Office Action enriched citations |
tsdr-swagger.json |
version 1 | TSDR (separate server + key) |
What this client implements from each ODP portal release:
| Portal release | Date | Changes affecting this client |
|---|---|---|
| 3.0 | 2025-11-21 | Initial PTAB datasets via ODP -- wrapped (19 endpoints). |
| 3.2 | 2026-01-16 | Office Action Weekly Archives surfaced as bulk product OACT -- accessible via the existing SearchBulkProducts / GetBulkProduct wrappers. PTAB RSS feed not wrapped (separate host, no API key). |
| 3.3 | 2026-03-04 | Trademark (TM) Decisions & Proceedings reachable only through the portal SPA at data.uspto.gov/ui/trademark/tm-decisions-api/, using cookie + WAF-token auth. No public API spec. Not wrapped; will revisit if USPTO publishes one. |
| 3.4 | 2026-03-13 | PatentsView bulk products surfaced via the existing bulk-data wrappers. |
| 3.5 | 2026-03-24 | Office Action APIs migrated to ODP -- wrapped (8 endpoints). The OA wrappers use the ODP host (Config.OABaseURL default https://api.uspto.gov). |
| 3.6 | 2026-04-10 | US PCT numbers standardized to 15-char (PCTUS...); display form is 17-char with slashes -- fully supported. Patent Assignments API: countryOrStateCode deprecated, location consolidated under geographicRegionCode -- handled with fallback. Daily/yearly bulk file resumption is dataset coverage, no client change. |
| 4.0 | (latest) | Portal-level changes; the API specs in this repo are still 1.0.x. If 4.0 ever introduces a path or schema change, drop the new swagger files into swagger/ and run go run ./cmd/gen; the coverage test catches anything that goes silently unwrapped. |
go get github.com/patent-dev/uspto-odpThe ODP API requires a free MyUSPTO (USPTO.gov) account and an ODP API key
issued from data.uspto.gov. Key issuance involves identity verification: your
USPTO.gov account must be linked to a validated ID.me identity. The same key
covers the Patent, PTAB, Petition, Bulk Data, and Office Action APIs and is sent
in the X-API-Key header. TSDR uses a separate key (see TSDR).
-
Sign in at MyODP, or create a USPTO.gov account from the sign-in page if you don't have one.
-
Verify your identity with ID.me and link it to your USPTO.gov account (required before a key can be issued).
-
Request an API key from the Getting Started page and copy it.
-
Export it for the client and demo:
export USPTO_API_KEY=... # optional, for TSDR (https://account.uspto.gov/profile/api-manager): export USPTO_TSDR_API_KEY=...
Unused ODP keys are deleted after 90 days of inactivity.
config := odp.DefaultConfig()
config.APIKey = os.Getenv("USPTO_API_KEY")
client, err := odp.NewClient(config)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
results, err := client.SearchPatents(ctx, "artificial intelligence", 0, 10)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d patents\n", *results.Count)All ODP API endpoints are wrapped, plus the Office Action and TSDR APIs (53 wrapper methods total).
// Core Patent Data
SearchPatents(ctx, query string, offset, limit int) (*PatentDataResponse, error)
GetPatent(ctx, patentNumber string) (*PatentDataResponse, error) // Accepts any patent number format
GetPatentMetaData(ctx, applicationNumber string) (*MetaDataResponse, error)
// Patent Details
GetPatentAdjustment(ctx, applicationNumber string) (*AdjustmentResponse, error)
GetPatentContinuity(ctx, applicationNumber string) (*ContinuityResponse, error)
GetPatentDocuments(ctx, applicationNumber string) (*DocumentBag, error)
GetPatentAssignment(ctx, applicationNumber string) (*AssignmentResponse, error)
GetPatentAssociatedDocuments(ctx, applicationNumber string) (*AssociatedDocumentsResponse, error)
GetPatentAttorney(ctx, applicationNumber string) (*RecordAttorney, error)
GetPatentForeignPriority(ctx, applicationNumber string) (*ForeignPriorityResponse, error)
GetPatentTransactions(ctx, applicationNumber string) (*TransactionsResponse, error)
// Downloads & Utilities
SearchPatentsDownload(ctx, req PatentDownloadRequest) ([]byte, error)
GetStatusCodes(ctx) (*StatusCodeSearchResponse, error)SearchBulkProducts(ctx, query string, offset, limit int) (*BdssResponseBag, error)
GetBulkProduct(ctx, productID string) (*BdssResponseProductBag, error)
// File download methods (use FileDownloadURI directly):
DownloadBulkFile(ctx, fileDownloadURI string, w io.Writer) error
DownloadBulkFileWithProgress(ctx, fileDownloadURI string, w io.Writer,
progress func(bytesComplete, bytesTotal int64)) errorproduct, err := client.GetBulkProduct(ctx, "PTGRXML")
files := *product.BulkDataProductBag[0].ProductFileBag.FileDataBag
for _, file := range files {
if file.FileName != nil && strings.Contains(*file.FileName, "ipg250923.zip") {
if file.FileDownloadURI != nil {
err := client.DownloadBulkFileWithProgress(ctx, *file.FileDownloadURI, outputFile,
func(bytesComplete, bytesTotal int64) {
percent := float64(bytesComplete) * 100 / float64(bytesTotal)
fmt.Printf("\rProgress: %.1f%%", percent)
})
}
break
}
}SearchPetitions(ctx, query string, offset, limit int) (*PetitionDecisionResponseBag, error)
GetPetitionDecision(ctx, recordID string, includeDocuments bool) (*PetitionDecisionIdentifierResponseBag, error)
SearchPetitionsDownload(ctx, req PetitionDecisionDownloadRequest) ([]byte, error)// Trial Proceedings (IPR, PGR, CBM)
SearchTrialProceedings(ctx, query string, offset, limit int) (*ProceedingDataResponse, error)
GetTrialProceeding(ctx, trialNumber string) (*ProceedingDataResponse, error)
SearchTrialProceedingsDownload(ctx, req DownloadRequest) ([]byte, error)
// Trial Decisions
SearchTrialDecisions(ctx, query string, offset, limit int) (*DecisionDataResponse, error)
GetTrialDecision(ctx, documentIdentifier string) (*DecisionDataResponse, error)
GetTrialDecisionsByTrialNumber(ctx, trialNumber string) (*DecisionDataResponse, error)
SearchTrialDecisionsDownload(ctx, req DownloadRequest) ([]byte, error)
// Trial Documents
SearchTrialDocuments(ctx, query string, offset, limit int) (*DocumentDataResponse, error)
GetTrialDocument(ctx, documentIdentifier string) (*DocumentDataResponse, error)
GetTrialDocumentsByTrialNumber(ctx, trialNumber string) (*DocumentDataResponse, error)
SearchTrialDocumentsDownload(ctx, req DownloadRequest) ([]byte, error)
// Appeal Decisions
SearchAppealDecisions(ctx, query string, offset, limit int) (*AppealDecisionDataResponse, error)
GetAppealDecision(ctx, documentIdentifier string) (*AppealDecisionDataResponse, error)
GetAppealDecisionsByAppealNumber(ctx, appealNumber string) (*AppealDecisionDataResponse, error)
SearchAppealDecisionsDownload(ctx, req DownloadRequest) ([]byte, error)
// Interference Decisions
SearchInterferenceDecisions(ctx, query string, offset, limit int) (*InterferenceDecisionDataResponse, error)
GetInterferenceDecision(ctx, documentIdentifier string) (*InterferenceDecisionDataResponse, error)
GetInterferenceDecisionsByNumber(ctx, interferenceNumber string) (*InterferenceDecisionDataResponse, error)
SearchInterferenceDecisionsDownload(ctx, req PatentDownloadRequest) ([]byte, error)These use the DSAPI pattern (form-encoded POST with Lucene/Solr queries). Same API key as the main ODP API.
// Office Action Text Retrieval
SearchOfficeActions(ctx, criteria string, start, rows int) (*DSAPIResponse, error)
GetOfficeActionFields(ctx) (*DSAPIFieldsResponse, error)
// Office Action Citations (Forms PTO-892 & PTO-1449)
SearchOfficeActionCitations(ctx, criteria string, start, rows int) (*DSAPIResponse, error)
GetOfficeActionCitationFields(ctx) (*DSAPIFieldsResponse, error)
// Office Action Rejections (101, 102, 103, 112, DP)
SearchOfficeActionRejections(ctx, criteria string, start, rows int) (*DSAPIResponse, error)
GetOfficeActionRejectionFields(ctx) (*DSAPIFieldsResponse, error)
// Enriched Citations (AI/ML extracted from office actions)
SearchEnrichedCitations(ctx, criteria string, start, rows int) (*DSAPIResponse, error)
GetEnrichedCitationFields(ctx) (*DSAPIFieldsResponse, error)Office Action APIs use Lucene query syntax:
// Search by patent application number
result, err := client.SearchOfficeActionRejections(ctx, "patentApplicationNumber:12190351", 0, 10)
// Search with boolean operators
result, err := client.SearchOfficeActions(ctx, "hasRej103:1 AND groupArtUnitNumber:1713", 0, 10)
// Get available fields
fields, err := client.GetOfficeActionRejectionFields(ctx)
fmt.Printf("Searchable fields: %v\n", fields.Fields)Separate server (tsdrapi.uspto.gov) and API key. Requires TSDRAPIKey in
config, sent in the USPTO-API-KEY header.
config := odp.DefaultConfig()
config.APIKey = "your-odp-key"
config.TSDRAPIKey = "your-tsdr-key" // from https://account.uspto.gov/profile/api-manager
client, err := odp.NewClient(config)
// Get trademark case status (raw XML response)
xmlResp, err := client.GetTrademarkStatus(ctx, "97123456")
// Get trademark case status (JSON via content negotiation)
status, err := client.GetTrademarkStatusJSON(ctx, "97123456")
fmt.Printf("Trademarks: %d\n", len(status.Trademarks))
// Get document list (XML)
docs, err := client.GetTrademarkDocumentsXML(ctx, "97123456")
// Get info about a specific document (XML) - docID format: {TypeCode}{YYYYMMDD}
info, err := client.GetTrademarkDocumentInfo(ctx, "97123456", "NOA20230322")
// Download a document as PDF
var buf bytes.Buffer
client.DownloadTrademarkDocument(ctx, "97123456", "NOA20230322", &buf)
// Get last update time
update, err := client.GetTrademarkLastUpdate(ctx, "97123456")
// Multi-status lookup
result, err := client.GetTrademarkMultiStatus(ctx, "sn", []string{"97123456", "97654321"})The library handles various patent number formats and automatically resolves them to application numbers:
// GetPatent accepts any patent number format
doc, err := client.GetPatent(ctx, "US 11,646,472 B2") // Grant number
doc, err := client.GetPatent(ctx, "17/248,024") // Application number
doc, err := client.GetPatent(ctx, "US20250087686A1") // Publication number
// For other methods, resolve to application number first
appNumber, err := client.ResolvePatentNumber(ctx, "US 11,646,472 B2")
// appNumber = "17248024" (the actual application number)
// Low-level normalization (formatting only, doesn't resolve)
pn, err := odp.NormalizePatentNumber("US 11,646,472 B2")
fmt.Println(pn.Type) // PatentNumberTypeGrant
fmt.Println(pn.Normalized) // "11646472" (normalized, not application number!)
fmt.Println(pn.FormatAsGrant()) // "11,646,472"Note: Grant and publication numbers are not the same as application numbers. The library uses the search API to resolve grant/publication numbers to their corresponding application numbers.
Supported formats:
- Applications:
17248024,17/248,024,US 17/248,024 - Grants:
11646472,11,646,472,US 11,646,472 B2 - Publications:
20250087686,US20250087686A1(kind code preserved when supplied:A2,A9, ...) - PCT:
PCTUS2025058371(15-char API form),PCT/US2025/058371(17-char display),PCTUS0719317(12-char legacy). Usepn.FormatAsPCT()for the display form.
Note: 8-digit numbers (like 11646472) are ambiguous - they could be either
grant or application numbers. Use formatting (commas, kind codes) to
disambiguate.
Parse full patent text (ICE DTD 4.6/4.7):
doc, err := client.GetPatentXML(ctx, "US 11,646,472 B2")
title := doc.GetTitle()
abstract := doc.GetAbstract().ExtractAbstractText()
claims := doc.GetClaims().ExtractAllClaimsTextFormatted()
description := doc.GetDescription().ExtractDescriptionText()Advanced usage:
// Get XML URL and type
xmlURL, docType, err := client.GetXMLURLForApplication(ctx, "17248024")
// Download with type hint
doc, err := client.DownloadXMLWithType(ctx, xmlURL, docType)
// Parse raw XML
data := []byte(/* XML content */)
doc, err = odp.ParseGrantXML(data) // or ParseApplicationXMLconfig := &odp.Config{
BaseURL: "https://api.uspto.gov", // Default
APIKey: "your-api-key",
UserAgent: "YourApp/1.0",
MaxRetries: 3, // Retry failed requests
RetryDelay: 1 * time.Second, // Base backoff between retries
Timeout: 30 * time.Second, // Request timeout
MaxRetryAfter: 60 * time.Second, // Longest Retry-After the client will honor
// Office Action DSAPI host (defaults to the ODP host)
OABaseURL: "https://api.uspto.gov", // Default (Office Action endpoints on the ODP host)
// TSDR (optional - separate server and API key)
TSDRAPIKey: "your-tsdr-key", // From https://account.uspto.gov/profile/api-manager
TSDRBaseURL: "https://tsdrapi.uspto.gov", // Default
}
client, err := odp.NewClient(config)DefaultConfig() fills in sane defaults; override only the fields you need.
Non-2xx responses surface as *APIError, carrying the status code, a message,
and a truncated response body for debugging. Use errors.As to inspect them:
results, err := client.SearchPatents(ctx, query, 0, 10)
if err != nil {
var apiErr *odp.APIError
if errors.As(err, &apiErr) {
fmt.Println("status:", apiErr.StatusCode)
fmt.Println("detail:", apiErr.Detail()) // message + server body
if apiErr.IsRetryable() { // 429 or 5xx
// back off and retry, or let the client's own retry handle it
}
}
return err
}On HTTP 429 the client honors the Retry-After header (capped by
Config.MaxRetryAfter, default 60s) instead of its exponential backoff. When
the server asks for a longer wait than the cap, the request fails with a
non-retryable *APIError so the caller decides rather than the client blocking.
make test # unit tests with the race detector
make lint # gofmt + go vetIntegration tests require USPTO_API_KEY (and optionally USPTO_TSDR_API_KEY):
export USPTO_API_KEY=your-odp-key
export USPTO_TSDR_API_KEY=your-tsdr-key # from https://account.uspto.gov/profile/api-manager
make test-integrationThe USPTO ODP API specification is distributed as multiple YAML files with $ref
references between them. The originals are downloaded from
USPTO ODP Swagger and stored in swagger/:
swagger/
swagger.yaml # Main ODP API spec (Patent, Bulk, Petition)
odp-common-base.yaml # Shared type definitions
trial-proceedings.yaml # PTAB trial proceedings
trial-decisions.yaml # PTAB trial decisions
trial-documents.yaml # PTAB trial documents
trial-appeal-decisions.yaml # PTAB appeal decisions
trial-interferences.yaml # PTAB interference decisions
trial-common.yaml # Shared PTAB types
oa-text-retrieval.yaml # Office Action Text Retrieval
oa-citations.yaml # Office Action Citations
oa-rejections.yaml # Office Action Rejections
oa-enriched-citations.yaml # Enriched Citations
tsdr-swagger.json # TSDR (Trademark) API
Do not edit files in swagger/ - these are the original USPTO specifications.
The cmd/gen tool (pure Go, no external dependencies) bundles and fixes the specs:
go run ./cmd/gen- Bundles ODP + PTAB YAML, resolving
$ref->swagger_fixed.yaml->generated/ - Bundles the 4 Office Action specs ->
swagger_oa_fixed.yaml->generated/oa/ - Copies the TSDR spec ->
swagger_tsdr_fixed.json->generated/tsdr/ - Applies fixes for mismatches between the specs and actual API responses
The USPTO swagger has several mismatches with actual API responses:
Type corrections:
frameNumber,reelNumber: string -> integer (API returns numbers)documentNumber: string -> integer (PTAB API returns numbers)- Error response
code: integer -> string (API returns"404", not404)
Structure fixes:
petitionIssueConsideredTextBag: array of objects -> array of stringscorrespondenceAddress: array -> object (Assignment API returns an object)DecisionData.statuteAndRuleBag,issueTypeBag: string -> arrayGetPatentAssignment.assignmentBag: single object -> array
Field-name fixes:
InterferenceDecisionRecord.decisionDocumentData->documentDataDecisionDataResponse.patentTrialDecisionDataBag->patentTrialDocumentDataBag(the trial-decisions search returns the array underpatentTrialDocumentDataBag; without the fix the slice silently unmarshals to nil whilecountparses fine)
Format fixes:
- Removed
format: date-timefrom fields returning non-RFC3339 values (e.g.lastModifiedDateTimereturns"2025-11-26T23:58:00"without a timezone) - Removed
format: datefrom fields that return datetimes or non-ISO dates (e.g.fileReleaseDatereturns"2025-09-23 00:57:53")
Endpoint fixes:
- Removed
/api/v1/patent/applications/text-to-search(defined but has no operations)
Office Action (DSAPI) fixes:
- Removed phantom path parameters (
dataset,versiondeclaredin: pathbut the paths are static) - Made
operationIdvalues unique across the four bundled specs (all four used identical IDs) - The Office Action APIs are served from the ODP host (
https://api.uspto.gov, theroute Office Action APIs to ODPmigration) under/api/v1/patent/oa/...; override the host withConfig.OABaseURLif needed.
TSDR fixes:
- Fixed the protocol-relative server URL (
//tsdrapi.uspto.gov/->https://tsdrapi.uspto.gov) GetDocumentInfoXml/GetCaseDocsInfoXmlreturn 406 withAccept: application/json(content negotiation is unsupported despite XML/JSON paths in the spec)- Removed
format: date-time/format: datefrom 64 fields - the API returns inconsistent date formats that breaktime.Timeparsing in generated code
Upstream bugs worked around:
trial-appeal-decisions.yaml:appelantData->appellantData,realPartyName->realPartyInInterestName,techCenterNumber->technologyCenterNumber,requestorData->thirdPartyRequesterData,documentTypeCategory->documentTypeDescriptionText,downloadURI->fileDownloadURItrial-common.yaml:downloadURI->fileDownloadURI,statuteAndRuleBag/issueTypeBagstring -> array,documentNumberstring -> integer
Part of the patent.dev open-source patent data ecosystem:
- epo-ops - EPO Open Patent Services client (bibliographic, full text, families, legal status, images)
- epo-bdds - EPO Bulk Data Distribution Service client (DOCDB, INPADOC, EP full text)
- dpma-connect-plus - DPMA Connect Plus client (patents, designs, trademarks)
The bulk-file-loader uses these libraries for automated patent data downloads.
MIT - Funktionslust GmbH / patent.dev.