PatentRef

Data version 20261027.2, as of 2026-10-27: the build these pages were checked against. API version 1.21.0.

Quick start

1. Get a key

  1. Open https://patentref.io/api/v1/meta/signup/ in a browser, enter an email and a password, tick the box that accepts the Terms of Service, the Privacy Policy and the Acceptable Use Policy, confirm the email. Sign-in is Supabase Auth; PatentRef never stores the password.
  2. Sign in on the same page. The page mints the key, or an agent does it directly: POST https://patentref.io/api/v1/meta/keys/ with the header Authorization: Bearer <Supabase access token> and the JSON body {"terms_version": "beta-2"} (the same as {"action": "create", "terms_version": "beta-2"}), sent once the Terms of Service, the Privacy Policy and the Acceptable Use Policy (https://patentref.io/docs/legal/terms, /docs/legal/privacy, /docs/legal/acceptable-use) are read and accepted. Without it the answer is 403 ERR_TERMS with {"error": true, "terms_version", "terms": {"terms", "privacy", "acceptable_use"}, "how"}, unless the account accepted that version at sign-up or with its last key; keys minted before keep working. The response is 201 with {"error": false, "created": true, "api_key": "...", "prefix": "...", "plan": "free", "monthly_limit", "per_minute_limit", "created_at", "rotated_from", "terms_version", "note"}. The key value is shown exactly once. The same POST once a key exists answers 200 with that key's record and api_key null. POST {"action": "rotate"} revokes the active key and mints a new one (201), with the terms as for a first key; the month's count stays with the account. GET /api/v1/meta/keys/ with the token lists the account's keys, newest first, as prefixes ({"error", "email", "keys": [{"prefix", "plan", "revoked", "created_at", "rotated_from", "terms_version"}]}). A new account's first key is refused for disposable email domains (403 ERR_AUTH) and capped at 20 a day per email domain (429 ERR_SIGNUP_LIMIT). An optional "source" in the body (the campaign code of the sign-up link, its utm_source: 1 to 32 letters, digits and hyphens) adds one to that ISO week's count for the code when the request creates a key; nothing is stored with the key or the account, the answer does not change, and a code outside the rule is ignored. The sign-up page sends it by itself when its address carries utm_source.
  3. Use the key on every data request: X-Api-Key: <key>. GET https://patentref.io/api/v1/meta/usage/ with the key (or with the bearer token instead, for the account) returns {"error": false, "plan", "key_prefix", "month", "count", "monthly_limit", "remaining", "per_minute_limit", "resets_at"}: plan is "free" for a Free key (monthly_limit 1000); an operator key has plan, monthly_limit and remaining null, which means no monthly limit. key_prefix is the first 8 characters of the key, never the key. Without a key it answers 403 {"error": true} with X-Status-Reason-Code ERR_KEY.

Free plan: 1,000 requests a month, 45 a minute (a fixed UTC clock minute). What counts toward the month: a request to a data path under /api/v1/ (outside /api/v1/meta/) that carried X-Api-Key and was answered below 500 other than 403 and 429. So 200, 400 and 404 answers count; 403, 429 and 5xx answers (the 501 of a deferred endpoint too) do not, and neither do the /api/v1/meta/ endpoints (usage, keys, health, status), the record pages /p/<patent_id> or the files at the site root. Keys minted from an account count per account, so rotating a key keeps the count. Over 45 in a minute the API answers 429 with Retry-After and the body {"detail": "Request was throttled. Expected available in N seconds."} until the minute turns. Over the month the same 429 until the first of the next month (UTC). Without a key, or with a revoked one, every data endpoint answers 403 with {"detail": "You do not have permission to perform this action."}, exactly as upstream did (the account endpoints under /meta/ add X-Status-Reason-Code ERR_KEY).

2. A request with curl

Set the key in the shell (export KEY=...), then:

curl -s -G -H "X-Api-Key: $KEY" https://patentref.io/api/v1/patent/ \
  --data-urlencode 'q={"patent_id": ["D345393", "10905426", "11172927"]}' \
  --data-urlencode 'f=["patent_id", "patent_title", "patent_date"]' \
  --data-urlencode 's=[{"patent_id": "asc"}]' \
  --data-urlencode 'o={"size": 200}'

3. The same in Python

With the requests package. GET takes each parameter as JSON text; POST takes them as one JSON body.

import json, os, requests

base_url = "https://patentref.io"
headers = {"X-Api-Key": os.environ["PATENTREF_KEY"]}
params = {"q": {"patent_year": "2021"}, "f": ["patent_id", "patent_title", "patent_date", "inventors"], "o": {"size": 3}}
r = requests.get(base_url + "/api/v1/patent/", headers=headers,
                 params={k: json.dumps(v) for k, v in params.items()})
r.raise_for_status()
data = r.json()
print(data["total_hits"], data["patents"][:3])
import json, os, requests

base_url = "https://patentref.io"
headers = {"X-Api-Key": os.environ["PATENTREF_KEY"]}
params = {"q": {"_and": [{"_gte": {"patent_date": "2020-01-01"}}, {"_lte": {"patent_date": "2020-12-31"}}]}, "f": ["patent_id", "patent_title", "patent_date"], "s": [{"patent_date": "desc"}], "o": {"size": 100}}
r = requests.post(base_url + "/api/v1/patent/", headers=headers, json=params)
r.raise_for_status()
data = r.json()
print(data["total_hits"], data["patents"][:3])

A detail route takes the key in the path: GET https://patentref.io/api/v1/patent/D345393/. The answer has the same shape as a list, with one record.

4. 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.

Next: the query grammar, the endpoints, examples.