A Go client library for the European Patent Office's Open Patent Services (OPS) API v3.2.
This library provides an idiomatic Go interface to interact with the EPO's Open Patent Services, allowing you to:
- Retrieve patent bibliographic data, claims, descriptions, and abstracts
- Search for patents using various criteria
- Get patent family information (INPADOC)
- Access CPC/ECLA classification data (schema, statistics, mapping, media)
- Download patent images and convert TIFF to PNG
- Access legal status and register data
- Track API quota usage
- OAuth2 authentication with automatic token management
- Patent text retrieval (biblio, claims, description, abstract, fulltext)
- Patent search using CQL (Contextual Query Language)
- INPADOC family retrieval
- CPC/ECLA classification services (schema, statistics, mapping, media)
- Patent image retrieval with TIFF to PNG conversion
- Legal status retrieval
- EPO Register access (biblio, events, procedural steps, unitary patent)
- Patent number format conversion
- Comprehensive error handling with custom error types
- Automatic retry logic with exponential backoff
- Quota tracking and fair use monitoring
- Unit and integration tests
go get github.com/patent-dev/epo-opspackage main
import (
"context"
"fmt"
"log"
ops "github.com/patent-dev/epo-ops"
)
func main() {
config := &ops.Config{
ConsumerKey: "your-consumer-key",
ConsumerSecret: "your-consumer-secret",
}
client, err := ops.NewClient(config)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Retrieve bibliographic data
biblio, err := client.GetBiblio(ctx, "publication", "docdb", "EP1000000")
if err != nil {
log.Fatal(err)
}
fmt.Println("Biblio:", biblio)
// Search patents
results, err := client.Search(ctx, "ti=plastic", "1-5")
if err != nil {
log.Fatal(err)
}
fmt.Println("Search results:", results)
// Get patent family
family, err := client.GetFamily(ctx, "publication", "docdb", "EP1000000B1")
if err != nil {
log.Fatal(err)
}
fmt.Println("Family:", family)
// Get patent image (first page of drawings)
imageData, err := client.GetImage(ctx, "EP", "1000000", "B1", "Drawing", 1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Retrieved image: %d bytes\n", len(imageData))
}This library provides two ways to access API data:
By default, methods return parsed Go structs for type-safe access:
// Returns *BibliographicData struct
biblio, err := client.GetBiblio(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Title: %s\n", biblio.InventionTitle)
fmt.Printf("Applicants: %d\n", len(biblio.Applicants))
// Returns *FamilyData struct
family, err := client.GetFamily(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Family ID: %s\n", family.FamilyID)
fmt.Printf("Members: %d\n", len(family.Members))
// Returns *SearchResultData struct
results, err := client.Search(ctx, "ti=battery", "1-5")
fmt.Printf("Total: %d\n", results.TotalResults)
for _, patent := range results.Patents {
fmt.Printf(" %s\n", patent.Number)
}
// Returns *LegalData struct
legal, err := client.GetLegal(ctx, "publication", "docdb", "EP1000000B1")
for _, event := range legal.LegalEvents {
fmt.Printf("%s: %s\n", event.Date, event.Code)
}For special cases (e.g., saving raw XML, custom parsing), use *Raw() methods:
// Returns raw XML string
xmlData, err := client.GetBiblioRaw(ctx, "publication", "docdb", "EP1000000B1")
os.WriteFile("biblio.xml", []byte(xmlData), 0644)
// All endpoints have Raw variants
familyXML, err := client.GetFamilyRaw(ctx, "publication", "docdb", "EP1000000B1")
searchXML, err := client.SearchRaw(ctx, "ti=battery", "1-5")
legalXML, err := client.GetLegalRaw(ctx, "publication", "docdb", "EP1000000B1")Architecture Note: Parsed methods internally call the corresponding *Raw() method and parse the result. This ensures consistent data access and eliminates code duplication.
To use the EPO OPS API, you need to register for API credentials:
- Visit https://developers.epo.org/
- Create an account or sign in
- Register a new application to get your consumer key and secret
The EPO OPS API has usage limits:
- Non-paying users: 4 GB/week (free)
- Paying users: >4 GB/week (€2,800/year)
See: https://www.epo.org/en/service-support/ordering/fair-use
This client automatically tracks quota usage from API responses:
// Make API calls
client.GetBiblio(ctx, "publication", "docdb", "EP1000000")
// Check quota status
quota := client.GetLastQuota()
if quota != nil {
fmt.Printf("Status: %s\n", quota.Status) // "green", "yellow", "red", or "black"
fmt.Printf("Individual: %d/%d (%.2f%%)\n",
quota.Individual.Used,
quota.Individual.Limit,
quota.Individual.UsagePercent())
}Patent images from EPO are typically in TIFF format. This library includes utilities to convert TIFF to PNG:
import (
ops "github.com/patent-dev/epo-ops"
"github.com/patent-dev/epo-ops/tiffutil"
)
// Retrieve patent image (TIFF format)
imageData, err := client.GetImage(ctx, "EP", "1000000", "B1", "Drawing", 1)
if err != nil {
log.Fatal(err)
}
// Convert TIFF to PNG (with automatic rotation for landscape images)
pngData, err := tiffutil.TIFFToPNG(imageData)
if err != nil {
log.Fatal(err)
}
// Save PNG file
os.WriteFile("patent_drawing.png", pngData, 0644)
// Or convert without rotation
pngData, err := tiffutil.TIFFToPNGNoRotate(imageData)
// Or batch convert multiple pages
pngImages, err := tiffutil.BatchTIFFToPNG([][]byte{imageData1, imageData2, imageData3})The TIFF utilities support:
- CCITT Group 3/4 compression (common in patent drawings)
- LZW compression
- CMYK color model
- Automatic landscape-to-portrait rotation
// Create client with default configuration
config := &ops.Config{
ConsumerKey: "your-key",
ConsumerSecret: "your-secret",
}
client, err := ops.NewClient(config)
// Create client with custom configuration
config := &ops.Config{
ConsumerKey: "your-key",
ConsumerSecret: "your-secret",
BaseURL: "https://ops.epo.org/3.2/rest-services", // Default
MaxRetries: 3, // Default
RetryDelay: time.Second, // Default
Timeout: 30 * time.Second, // Default
}
client, err := ops.NewClient(config)All methods return parsed Go structs by default. Use *Raw() variants for XML access.
// Retrieve bibliographic data → *BibliographicData
biblio, err := client.GetBiblio(ctx, "publication", "docdb", "EP1000000B1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Title: %s\n", biblio.InventionTitle)
fmt.Printf("Publication Date: %s\n", biblio.PublicationDate)
for _, applicant := range biblio.Applicants {
fmt.Printf("Applicant: %s\n", applicant.Name)
}
// Retrieve claims → *ClaimsData
claims, err := client.GetClaims(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Claims count: %d\n", len(claims.Claims))
for _, claim := range claims.Claims {
fmt.Printf("Claim %s: %s\n", claim.Number, claim.Text)
}
// Retrieve description → *DescriptionData
description, err := client.GetDescription(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Paragraphs: %d\n", len(description.Paragraphs))
// Retrieve abstract → *AbstractData
abstract, err := client.GetAbstract(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Abstract: %s\n", abstract.Text)
// Retrieve full text → *FulltextData (biblio + abstract + description + claims)
fulltext, err := client.GetFulltext(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Title: %s\n", fulltext.Biblio.InventionTitle)
fmt.Printf("Abstract: %s\n", fulltext.Abstract.Text)
fmt.Printf("Description paragraphs: %d\n", len(fulltext.Description.Paragraphs))
fmt.Printf("Claims: %d\n", len(fulltext.Claims.Claims))
// Get published equivalents (simple family) → *EquivalentsData
equivalents, err := client.GetPublishedEquivalents(ctx, "publication", "docdb", "EP1000000B1")
fmt.Printf("Equivalents: %d\n", len(equivalents.Equivalents))
for _, eq := range equivalents.Equivalents {
fmt.Printf(" %s (Date: %s, Kind: %s)\n", eq.DocNumber, eq.Date, eq.Kind)
}
// Raw XML access (if needed)
xmlData, err := client.GetBiblioRaw(ctx, "publication", "docdb", "EP1000000B1")
os.WriteFile("biblio.xml", []byte(xmlData), 0644)Parameters:
refType: Reference type -"publication","application", or"priority"format: Number format -"docdb"or"epodoc"number: Patent number (e.g.,"EP1000000B1")
Returns *SearchResultData with parsed results.
// Basic search → *SearchResultData
results, err := client.Search(ctx, "ti=battery", "1-25")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total results: %d\n", results.TotalResults)
fmt.Printf("Range: %s\n", results.Range)
fmt.Printf("Returned: %d patents\n", len(results.Patents))
for _, patent := range results.Patents {
fmt.Printf("Patent: %s\n", patent.Number)
fmt.Printf(" Country: %s, Date: %s, Kind: %s\n",
patent.Country, patent.Date, patent.Kind)
}
// Search with specific constituent → *SearchResultData
results, err := client.SearchWithConstituent(ctx, "biblio", "pa=Siemens", "1-10")
// Raw XML access
xmlData, err := client.SearchRaw(ctx, "ti=battery", "1-25")CQL Query Examples:
ti=plastic- Title contains "plastic"pa=Siemens- Applicant is Siemensti=plastic and pa=Siemens- Combined searchde- Country code DE
Range Format: "1-25" (default), "1-100", etc.
Returns *FamilyData with parsed family information.
// Basic INPADOC family → *FamilyData
family, err := client.GetFamily(ctx, "publication", "docdb", "EP1000000B1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Family ID: %s\n", family.FamilyID)
fmt.Printf("Patent Number: %s\n", family.PatentNumber)
fmt.Printf("Total Members: %d\n", family.TotalCount)
fmt.Printf("Has Legal Data: %v\n", family.Legal)
for _, member := range family.Members {
fmt.Printf("Member: %s %s %s (Date: %s)\n",
member.Country, member.DocNumber, member.Kind, member.Date)
if member.ApplicationRef.DocNumber != "" {
fmt.Printf(" Application: %s %s (Date: %s)\n",
member.ApplicationRef.Country,
member.ApplicationRef.DocNumber,
member.ApplicationRef.Date)
}
for _, priority := range member.PriorityClaims {
fmt.Printf(" Priority: %s %s (Date: %s)\n",
priority.Country, priority.DocNumber, priority.Date)
}
}
// Family with bibliographic data → *FamilyData
family, err := client.GetFamilyWithBiblio(ctx, "publication", "docdb", "EP1000000B1")
// Family with legal status → *FamilyData
family, err := client.GetFamilyWithLegal(ctx, "publication", "docdb", "EP1000000B1")
// Raw XML access
xmlData, err := client.GetFamilyRaw(ctx, "publication", "docdb", "EP1000000B1")// Retrieve patent image (typically TIFF format)
imageData, err := client.GetImage(ctx, "EP", "1000000", "B1", "Drawing", 1)
// Image types: "FullDocument", "Drawing", "FirstPageClipping"
// Page: 1-based page number// Legal status data → *LegalData
legal, err := client.GetLegal(ctx, "publication", "docdb", "EP1000000B1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Patent: %s\n", legal.PatentNumber)
fmt.Printf("Legal Events: %d\n", len(legal.LegalEvents))
for _, event := range legal.LegalEvents {
fmt.Printf("Event: %s (Code: %s)\n", event.Date, event.Code)
fmt.Printf(" Country: %s\n", event.Country)
if event.Description != "" {
fmt.Printf(" Description: %s\n", event.Description)
}
if event.Status != "" {
fmt.Printf(" Status: %s\n", event.Status)
}
}
// Raw XML access
xmlData, err := client.GetLegalRaw(ctx, "publication", "docdb", "EP1000000B1")
// EPO Register bibliographic data (returns raw XML)
registerBiblio, err := client.GetRegisterBiblioRaw(ctx, "publication", "docdb", "EP1000000B1")
// EPO Register procedural events (returns raw XML)
events, err := client.GetRegisterEventsRaw(ctx, "publication", "docdb", "EP1000000B1")// Convert patent number formats
converted, err := client.ConvertPatentNumber(ctx, "publication", "docdb", "EP1000000B1", "epodoc")Formats:
original:US.(05/948,554).19781004epodoc:US19780948554docdb:US 19780948554
// Get last quota information
quota := client.GetLastQuota()
if quota != nil {
fmt.Printf("Status: %s\n", quota.Status)
fmt.Printf("Usage: %.2f%%\n", quota.Individual.UsagePercent())
}| Option | Type | Default | Description |
|---|---|---|---|
ConsumerKey |
string | required | OAuth2 consumer key |
ConsumerSecret |
string | required | OAuth2 consumer secret |
BaseURL |
string | https://ops.epo.org/3.2/rest-services |
API base URL |
MaxRetries |
int | 3 |
Maximum retry attempts |
RetryDelay |
time.Duration | 1s |
Base delay between retries |
Timeout |
time.Duration | 30s |
HTTP client timeout (increase for bulk classification endpoints) |
The library provides custom error types for different failure scenarios:
biblio, err := client.GetBiblio(ctx, "publication", "docdb", "EP1000000B1")
if err != nil {
switch e := err.(type) {
case *ops.AuthError:
// Authentication failed
log.Printf("Auth error: %v", e)
case *ops.NotFoundError:
// Patent not found (404)
log.Printf("Patent not found: %v", e)
case *ops.QuotaExceededError:
// Fair use limit exceeded
log.Printf("Quota exceeded: %v", e)
case *ops.ServiceUnavailableError:
// Temporary service outage
log.Printf("Service unavailable: %v", e)
default:
// Other errors
log.Printf("Error: %v", err)
}
}Available Error Types:
AuthError- Authentication failuresNotFoundError- Resource not found (404)QuotaExceededError- Fair use quota exceeded (429, 403)ServiceUnavailableError- Temporary service outage (503)AmbiguousPatentError- Multiple kind codes availableConfigError- Configuration issues
The client automatically retries failed requests with exponential backoff:
- Retryable: 5xx errors, 408, timeouts, network errors
- Non-retryable: 404, 400, authentication errors, quota exceeded
- Token refresh: Automatic on 401 errors
- Backoff: Exponential with base delay × (attempt + 1)
Example with custom retry configuration:
config := &ops.Config{
ConsumerKey: "your-key",
ConsumerSecret: "your-secret",
MaxRetries: 5, // Try up to 5 times
RetryDelay: 2 * time.Second, // Start with 2s delay
}
client, err := ops.NewClient(config)Run unit tests:
go test -vRun integration tests (requires credentials):
export EPO_OPS_CONSUMER_KEY="your-key"
export EPO_OPS_CONSUMER_SECRET="your-secret"
go test -tags=integration -vSee the demo/ directory for a complete example application demonstrating all features.
This library uses an OpenAPI 3.0 specification that was:
- Converted from the official EPO OPS Swagger 2.0 specification
- Extended to include the Data Usage Statistics endpoint (missing from official spec)
- Enhanced with proper type definitions for generated code
The specification is maintained in openapi.yaml and used to generate strongly-typed client code.
MIT License - see LICENSE file for details.
Developed by:
- Wolfgang Stark - patent.dev - Funktionslust GmbH