Query grammar
The PatentSearch v1 grammar, as the API applies it. The OpenAPI file carries the same rules as JSON schemas: the Query schema for q, each endpoint's Fields and Sort schemas for f and s, the Options schema for o.
The request
Every endpoint accepts GET with four query parameters, or POST with the same four as a JSON body. Each parameter is a JSON value, URL-encoded on GET. The OpenAPI file gives each one a JSON schema (content application/json): q the Query schema (the grammar below), f the endpoint's <Name>Fields schema (every field name it accepts), s its <Name>Sort schema (the sortable fields), o the Options schema. On a list endpoint q is required (400 ERR_Q without it), except on the lookup endpoints (the cpc, uspc, ipc and wipo lists), which list every record when q is omitted.
- q: the query. One JSON object with exactly one top-level key.
- f: a JSON list of field names to return. Omitted: the endpoint's default fields (listed per endpoint in the endpoints page and in the OpenAPI file). Nested groups use dotted names, for example "inventors.inventor_name_last"; naming a group ("inventors") returns every field of the group.
- s: a JSON list of single-key objects, {"field": "asc"} or {"field": "desc"}. A default sort always applies (patent_id ascending on /patent/); relevance never orders results.
- o: options. size (default 100, maximum 1000; larger values are capped at 1000), after (the cursor, the grammar page), pad_patent_id (default false), exclude_withdrawn (default true on /patent/).
Example: GET https://patentref.io/api/v1/patent/?q={"patent_id":"7861317"}&f=["patent_id","patent_title","patent_date"]
curl -G -H "X-Api-Key: $KEY" https://patentref.io/api/v1/patent/ --data-urlencode 'q={"patent_id":"7861317"}' --data-urlencode 'f=["patent_id","patent_title","patent_date","inventors.inventor_name_last"]'
Sending a request
- GET query strings: percent-encode each value, as curl --data-urlencode and Python's requests do; that always works. Raw JSON works too, as in the example above, with four exceptions: a space must be sent as %20 (a request line cannot hold one), a + is read as a space (send a literal + as %2B), & and # end the value (%26, %23), and a literal % must be sent as %25.
- Request line: on GET the whole request line (GET, the path with its query string, and HTTP/1.1) may be at most 4,094 bytes. A longer one answers 400 with an HTML page from the web server in front of the API and no X-Status-Reason-Code, before the API reads it. Send a long q, such as a list of a few hundred ids, by POST.
- POST body: at most 64,000 bytes. A larger body answers 413 with an empty body and no X-Status-Reason-Code, from the web server in front of the API, with or without a key. Inside that, q may be at most 16 KB (16,384 bytes): a larger q answers 400 ERR_Q ('q' parameter too large: N bytes, the limit is 16384).
- Trailing slash: every documented path ends in "/". A list route (/patent/, /inventor/ and the rest) answers the same with or without it, GET and POST, with no redirect. A detail route needs it: /patent/D345393 answers 301 to /patent/D345393/ with the query string kept, which a GET client follows; a POST body does not follow a redirect, so POST to the form with the slash.
Operators in q
- Plain key: equality. {"patent_id": "7861317"}. On a text field a plain key is an analyzed match (any of the words), as upstream; on a keyword field, and on a text field with a keyword form, it is exact and case-sensitive (see Field kinds below).
- A list value: any of. {"inventors.inventor_name_last": ["Whitney", "Hopper"]}.
- _eq, _neq: equality and inequality. {"_neq": {"patent_type": "design"}}.
- _gt, _gte, _lt, _lte: ranges on dates and numbers. {"_gte": {"patent_date": "2020-01-01"}}. A partial date spans its unit: "2010" is the whole year, "2010-06" the month.
- _begins: case-insensitive prefix on string fields. {"_begins": {"assignees.assignee_organization": "Apple"}}.
- _contains: case-insensitive substring on string fields. {"_contains": {"patent_title": "laser"}}. On a text field it matches within one word, as upstream: a needle with a space or punctuation matches nothing. On a keyword field or a text field with a keyword form it tests the whole value, so on /assignee/ {"_contains": {"assignee_organization": "Tesla"}} finds "Tesla, Inc.", "TESLA MEDICAL S.R.O." and "Eaglestone Investment Partners I, L.P. Tesla Capital LLC".
- _text_all, _text_any, _text_phrase: full-text search on text fields: all words, any word, the words in this order. {"_text_any": {"patent_abstract": "quantum dot display"}}. Words are matched as tokens (SQLite FTS5 unicode61): case and punctuation such as hyphens are ignored, so _text_phrase "solid state battery" also matches "solid-state battery"; accents count (an accented letter does not match the plain one), and there is no stemming ("battery" does not match "batteries").
- _and, _or: a list of criteria. {"_and": [{"_gte": {"patent_date": "2020-01-01"}}, {"_text_phrase": {"patent_title": "neural network"}}]}.
- _not: negation of one criterion. {"_not": {"patent_type": "design"}}.
Field kinds
Every field of every record schema in the OpenAPI file, the fields of nested groups and the links included, carries x-patentref-kind, the way the server treats it:
- keyword: a plain key or _eq is exact and case-sensitive; _begins and _contains are case-insensitive on the whole value; sortable. Ids and codes are keyword fields (patent_id, assignee_id, cpc_group_id).
- text: _text_all, _text_any and _text_phrase search its words; a plain key or _eq matches any of the words; _begins and _contains test single words; not sortable. patent_title and patent_abstract are text fields.
- text+keyword: a text field with a keyword form. The full-text operators search its words, while a plain key, _eq, _begins and _contains use the whole value as on a keyword field; sortable. Names and organizations are mostly of this kind (assignees.assignee_organization, inventors.inventor_name_last).
- date and number: equality and the ranges _gt, _gte, _lt, _lte; a partial date spans its unit. boolean: equality. A number field (patent_year, the citation counts and the other numbers) takes a JSON number or a string that reads as one: {"patent_year": "2021"} matches what {"patent_year": 2021} matches, spaces around the digits are ignored, and "2021.0" is 2021. A value with a decimal part on a whole-number field matches nothing ({"patent_year": "2021.5"}: 0 hits), as upstream's Elasticsearch did, and any other string answers 400 ERR_Q (Invalid number supplied For input string: "abc").
- Country codes: the country fields of inventors, assignees and locations (inventors.inventor_country, assignees.assignee_country, location_country, inventor_lastknown_country, assignee_lastknown_country) hold ISO 3166-1 alpha-2 codes in capitals, as PatentsView gives them: the United Kingdom is "GB", and "UK" matches nothing. On a field of kind keyword or text+keyword a plain key is exact and case-sensitive, so "gb" matches nothing there either; inventor_lastknown_country and assignee_lastknown_country are text fields, where "gb" matches as "GB" does. The office fields of foreign citations and priority claims (citation_country, foreign_priority.foreign_country_filed) hold the code printed on the grant: mostly the two-letter office codes (GB, EP for the European Patent Office, WO for international applications under the PCT), with "UK" on some older rows, so query both "GB" and "UK" there.
- none: returned but not searchable; a query on it matches nothing, as upstream.
- f-only: the fields of the lapse group (the endpoints page): returned when f names them ("lapse" or "lapse.<field>"); q or s on them answers 400 ERR_Q (on q the reason is "the 'lapse' group is returned with f only", on s "Invalid field: lapse.<field>").
- Links: a field with format uri (inventors.inventor, assignees.assignee, cpc_current.cpc_class and the other classification links, the patent links of the citation records, assignee_lastknown_location) returns the URL of the detail record and takes the kind of the id behind it: keyword when q on it matches that id ({"cpc_current.cpc_class": "H01"}, not the URL), none when the index has no search field for it (inventors.inventor and assignees.assignee: query inventors.inventor_id or assignees.assignee_id instead). The URL is the detail route of the record, https://patentref.io/api/v1/<endpoint>/<id>/ with a "/" in the id written ":": inventors.inventor is https://patentref.io/api/v1/inventor/<inventor_id>/ (for example https://patentref.io/api/v1/inventor/fl:tr_ln:westlind-1/), assignees.assignee https://patentref.io/api/v1/assignee/<assignee_id>/, cpc_current.cpc_group https://patentref.io/api/v1/cpc_group/<cpc_group_id>/ (A01B63/114 as A01B63:114), and the patent links https://patentref.io/api/v1/patent/<patent_id>/. inventors.inventor and assignees.assignee come back beside their id: f naming inventors.inventor_id (or the whole group) returns inventor_id and the link, while f naming inventors.inventor alone returns no inventors group at all (assignees.assignee and assignees.assignee_id alike). A row without an id (a weekly grant, the data page) has the link null.
Sorting takes top-level fields only; each endpoint's <Name>Sort schema lists the ones it sorts on.
Rules that produce a 400 ERR_Q (the same list as https://patentref.io/docs/errors/ERR_Q): malformed JSON in q, f, s or o; no q on a list endpoint other than the lookup endpoints; more than one top-level key in q; an unknown field name; a field that does not belong to the endpoint; a sort on a text field without a keyword form; q or s on the lapse group; "offset" in o (the message is upstream's: 'offset' has been replaces with 'after' parameter.); a query over the cost cap; q over 16 KB (16,384 bytes) in a POST body. Prefix and wildcard operators on numeric, date and boolean fields answer 500 ERR_ES, as upstream's Elasticsearch did.
Cost cap: a query may hold at most 64 leaf criteria at depth 8 and a weighted cost of 100 units (every documented example is far below it); over the cap the API answers 400 ERR_Q naming the cost. On the long-text endpoints the cap also refuses _contains, a range or an empty _begins on the text field unless patent_id sits in the same _and (the endpoints page). The size limits are under Sending a request above: a POST body over 64,000 bytes answers 413 and a GET request line over 4,094 bytes answers 400, both from the web server in front of the API and without X-Status-Reason-Code, and q over 16 KB answers 400 ERR_Q. A statement that runs over 20 seconds, or a request over 30 seconds, answers 500 ERR_ES with the reason.
The response and paging
Shape: {"error": false, "count": <records in this page>, "total_hits": <exact number of matching records>, "<key>": [ ... ]}. The key is the endpoint's response key (the endpoints page), for example "patents". Nested groups come back as lists of objects; a group with no rows is omitted from the record.
Order of the rows in a nested group: the order of their source. For PatentsView data, inventors, assignees and attorneys come by inventor_sequence, assignee_sequence and attorney_sequence (0 first), and every other group (cpc_current, applicants, us_related_documents and the rest) in the row order of its PatentsView bulk file; for a weekly grant (the data page), the order of the grant's XML; inventor_years and assignee_years by year, oldest first. Every request against one data build gets the same order. Upstream promised no order either, and its order can differ: the saved output of the PatentsView code examples notebook lists the inventors of some patents in another order. Where the order matters, sort the rows on the group's *_sequence field.
Paging uses a cursor, never an offset. Take the sort values of the last record of a page and pass them as o.after on the next request, in the order of s. With the default sort on /patent/ (patent_id ascending):
page 1: q={"_gte":{"patent_date":"2024-01-01"}}&o={"size":1000}
page 2: same q and s, o={"size":1000,"after":["<patent_id of the last record of page 1>"]}
Walk until count is 0. The cursor is the raw sort values, so when s has two fields, after has two values. A walk is consistent inside one data build; check X-Data-Version if a walk spans a weekly refresh.
Every response carries X-Data-Version: the data_version of the search file in service (for example 20261006.5: the USPTO release date, a dot, the build sequence). Responses that never opened the data file (403, the account endpoints) carry the same value. The long text (the endpoints page) comes from a file of its own, which can be of an earlier build: X-Data-Version on those endpoints still names the search file, and X-Text-Data-Version on the same answer names the long-text file (the same value as text_data_version in GET /meta/health/), the version to cache long text under. The claim trees of /structure/claims/ (the endpoints page) are read from the long-text file too and carry both headers.