# PatentRef: the full guide for agents PatentRef is a hosted US patent data API compatible with the USPTO's PatentSearch API (the PatentsView API), which the USPTO paused on 2026-03-20. PatentRef serves the same grammar from its own snapshot of the USPTO PatentsView bulk data. This file is the whole guide: an agent that has read it and the OpenAPI file (https://patentref.io/openapi.json) can use the API without any other document. Base URL: https://patentref.io/api/v1/ Key header: X-Api-Key Data version header on every response: X-Data-Version Build facts: https://patentref.io/api/v1/meta/ Status: https://patentref.io/api/v1/meta/status/ (JSON at https://patentref.io/api/v1/meta/status.json) Docs for a person: https://patentref.io/docs/ (the same facts as pages: one per endpoint, the grammar, the Lapse fields, the claim trees, the claim counts, the known differences, moving from PatentsView) Error codes: https://patentref.io/docs/errors/ (one page per code at /docs/errors/) Machine description: https://patentref.io/openapi.json (OpenAPI 3.1) Short index: https://patentref.io/llms.txt Version: 1.21.0 (info.version in the OpenAPI file); what changed, newest first: https://patentref.io/changes.txt (MINOR goes up when answers or required inputs change, PATCH when only the documents do) No key needed for: /api/v1/meta/, /api/v1/meta/health/, the status page and status.json, the home page /, the docs at /docs/, /docs/errors/, /p/, the demo /try/ (four fixed requests, each shown as curl and answered on an allowance shared by every visitor, 20 runs an hour from one address; it takes no key and no parameter), and these files. https://patentref.io/robots.txt says what crawlers may read; every answer from patentref.io carries Strict-Transport-Security (max-age one year, includeSubDomains), so a browser uses HTTPS only. Every other /api/v1/ path needs X-Api-Key. ## 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 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: . 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/ 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. 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 Fields schema (every field name it accepts), s its 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 section 4 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, section 3), 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: 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 (section 4): returned when f names them ("lapse" or "lapse."); 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."). - 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/// with a "/" in the id written ":": inventors.inventor is https://patentref.io/api/v1/inventor// (for example https://patentref.io/api/v1/inventor/fl:tr_ln:westlind-1/), assignees.assignee https://patentref.io/api/v1/assignee//, cpc_current.cpc_group https://patentref.io/api/v1/cpc_group// (A01B63/114 as A01B63:114), and the patent links https://patentref.io/api/v1/patent//. 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, section 6) has the link null. Sorting takes top-level fields only; each endpoint's 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 (section 4). 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. ## 3. The response and paging Shape: {"error": false, "count": , "total_hits": , "": [ ... ]}. The key is the endpoint's response key (section 4), 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 (section 6), 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":[""]} 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 (section 4) 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/ (section 4) are read from the long-text file too and carry both headers. ## 4. Endpoints Every path is under https://patentref.io/api/v1/. A list endpoint answers q; a detail endpoint takes the key in the path (for example /patent/7861317/) and answers 404 {"detail": "Not found."} for an unknown key. On a detail route q and s are accepted and ignored (the path key selects the record; f and o apply), and a withdrawn patent's detail route answers 200 whatever o.exclude_withdrawn says (only the list route hides withdrawn patents). A "/" in a key is written ":" in the path: CPC group G01S7/4811 is /cpc_group/G01S7:4811/ and USPC subclass 100/1 is /uspc_subclass/100:1/, as the links inside records show; the slash form answers 404. Any field may be null where the bulk data has no value (for example patent_term_extension, and patent_detail_desc_length and the two average processing-day fields, which the build does not fill). Default fields in parentheses. Granted patents and their groups: - /patent/ and /patent/{patent_id}/ (patent_id, patent_title, patent_date), key patents. Nested groups: applicants, application, assignees, attorneys, botanic, cpc_at_issue, cpc_current, examiners, figures, foreign_priority, gov_interest_contract_award_numbers, gov_interest_organizations, granted_pregrant_crosswalk, inventors, ipcr, pct_data, us_related_documents, us_term_of_grant, uspc_at_issue, wipo. Top-level fields include patent_type, patent_abstract, patent_year, patent_num_times_cited_by_us_patents, patent_num_us_patents_cited, patent_num_foreign_documents_cited, patent_earliest_application_date, patent_term_extension, gov_interest_statement, withdrawn. The full field list is in the OpenAPI file under the PatentRecord schema. - /patent/us_patent_citation/ and /{patent_id}/ (patent_id, citation_patent_id, citation_date), key us_patent_citations: the patents a patent cites. Forward citations of X: q={"citation_patent_id":"X"}. - /patent/us_application_citation/ and /{patent_id}/ (patent_id, citation_document_number, citation_sequence), key us_application_citations. - /patent/foreign_citation/ (patent_id, citation_sequence, citation_number), key foreign_citations. - /patent/other_reference/ (patent_id, reference_sequence, reference_text), key other_references. /patent/otherreference/ answers the same: it is the path upstream used before its release 2.2.0 (2024-11-06), which the PatentsView code examples notebook still names. - /patent/rel_app_text/ (patent_id, related_text), key rel_app_texts. - /patent/attorney/ and /{attorney_id}/ (attorney_id, attorney_name_first, attorney_name_last, attorney_organization), key attorneys. Entities: - /inventor/ and /{inventor_id}/ (inventor_id, inventor_name_first, inventor_name_last), key inventors; nested inventor_years. - /assignee/ and /{assignee_id}/ (assignee_id, assignee_individual_name_first, assignee_individual_name_last, assignee_organization), key assignees; nested assignee_years. - /location/ and /{location_id}/ (location_id, location_name, location_state, location_country), key locations. Classifications: - /cpc_class/, /cpc_subclass/, /cpc_group/ and their detail routes, keys cpc_classes, cpc_subclasses, cpc_groups. - /uspc_mainclass/, /uspc_subclass/ and their detail routes, keys uspc_mainclasses, uspc_subclasses. - /ipc/ (ipc_id, ipc_section, ipc_class, ipc_subclass), key ipcr: the distinct section, class and subclass ids; per-patent IPC rows are the ipcr group on /patent/. - /wipo/ and /{wipo_id}/ (wipo_id, sector_title, field_title), key wipo. - /nber_category/, /nber_subcategory/ and their detail routes: dead routes upstream (no data set); they answer 501. - Not in the build in service yet: /cpc_class/, /cpc_subclass/, /uspc_mainclass/ and /wipo/ with their detail routes answer 501 ERR_NOT_IMPLEMENTED (X-Status-Reason names the data set). The same classifications come with each patent in its cpc_current, cpc_at_issue, uspc_at_issue and wipo groups, and /cpc_group/, /uspc_subclass/ and /ipc/ answer. Any endpoint whose data set a build does not carry answers that 501, which is why the OpenAPI file declares it on every route outside /patent/. Long text of granted patents (claims, brief summaries, detailed descriptions, drawing descriptions): - /g_claim/ (patent_id, claim_sequence, claim_text), /g_brf_sum_text/ (patent_id, summary_text), /g_detail_desc_text/ (patent_id, description_text), /g_draw_desc_text/ (patent_id, draw_desc_sequence, draw_desc_text). Served since 2026-10-08 from the long-text build 20261006.4 (the X-Text-Data-Version of their answers), made from the PatentsView long-text release of 2026-04-10 for the patents of the PatentsView granted release, so the last grant date covered is 2025-12-30 in all four: /g_claim/ holds the claims of patents granted 1976 to 2025-12-30 (136 million rows), /g_brf_sum_text/ the brief summaries of 1976 to 2025-12-30, /g_detail_desc_text/ the detailed descriptions of 2005 to 2025-12-30, /g_draw_desc_text/ the drawing descriptions of 1976 to 2025-12-30. The patents granted after 2025-12-30 (the weekly grants, in /patent/ and the lapse group) have no long text yet: these endpoints answer 0 rows for them until a long-text build covers them. Their answers carry X-Text-Data-Version, the long-text file's data_version; GET /meta/health/ shows the same value as text_data_version (null when no long-text file is in service; the four endpoints then answer 501 ERR_NOT_IMPLEMENTED and send no X-Text-Data-Version). - Claim numbering: claim_sequence counts from 0, so claim 1 is claim_sequence 0, as every *_sequence field in the API; claim_number is the printed number as five digits ("00001"); claim_dependent is null for an independent claim and names the parent for a dependent one ("claim 1"); exemplary is "1" for the exemplary claim, else "0". - Searching long text: use _text_all, _text_any or _text_phrase (full-text indexes; a phrase over all claims answers in about a second). _contains matches within one word, so a needle with a space or punctuation answers 0 rows at once, as upstream. _contains with a one-word needle, a range (_gt, _gte, _lt, _lte) or an empty _begins on claim_text, summary_text, description_text or draw_desc_text has no index to use: without patent_id in the same _and it answers 400 ERR_Q, "Query too expensive: ... reads and decompresses every row of ..."; with patent_id (one value or a list of up to 1,000) it runs, for example {"_and": [{"patent_id": ["10000000", "10000001"]}, {"_contains": {"claim_text": "laser"}}]} (1,000 patents of claims in about 0.4 s). A query that matches nearly every row, such as {"_neq": {"patent_id": ""}} sorted by patent_id (how the R package finds the first and last patent), answers in milliseconds with an exact total_hits. Deferred (pre-grant publications, not in this snapshot yet): /publication/, /publication/{document_number}/, /publication/rel_app_text/, /pg_claim/, /pg_brf_sum_text/, /pg_detail_desc_text/, /pg_draw_desc_text/. Each answers 501 with X-Status-Reason-Code ERR_NOT_IMPLEMENTED and the upstream body {"error": true}. PatentRef additions: expiry and maintenance-fee status (X-Api-Key, as every data path): - The lapse group on /patent/ and /patent/{patent_id}/: add "lapse" (or "lapse.") to f. One object in a list, like every nested group: expiry_status (active, expired_term, expired_fee_nonpayment, withdrawn, unknown), expiry_date_estimated (the statutory term end, computed under 35 USC 154 from the filing or earliest parent filing date, the grant date, the patent term adjustment and a dated terminal disclaimer, or for a patent on the USPTO 35 USC 156 list the list's original expiration date plus its extension; it is not a date the USPTO record prints, and it is null when the record and the list disagree on whether the term has passed, or when the record contradicts the filing date of a parent the term could count from and PatentRef holds no other USPTO record that gives it), expiry_basis (codes: term_20y, term_17y, term_14y, term_15y, terminal_disclaimer, pta_days:N, fee_lapse:YYYY-MM-DD, reinstated:YYYY-MM-DD, pte_156:N), reinstatement_possible (true, false or "unknown", under 37 CFR 1.378), fee_status ("lapsed" or null), lapse_date (the date of the maintenance-fee record's expiry entry), reinstated_date, prior and prior_pct (the Lapse history-match score and its percentile; null until computed on the server), fwd_early (forward citations within 10 years of grant, by US utility patents in PatentsView and in the weekly grants after it), fwd_late (15 or more years after), sleeper_flag (expired and quiet), as_of, data_version. The group is never in the default fields, and q or s on it answers 400 ERR_Q (on q the reason is "the 'lapse' group is returned with f only"); its fields are of kind f-only in the OpenAPI file. - The term rule behind expiry_date_estimated: utility and plant patents filed on or after 1995-06-08 run 20 years from filing, or from the filing of the earliest parent application the record lists (continuation, division or continuation-in-part, including a parent international application the record lists as a related publication; never a provisional or a foreign priority), plus the patent term adjustment (pta_days:N); where the record lists no parent rows (older grants often have none, for example US 5000000), the date counts from the patent's own filing. A parent's filing date that the record itself contradicts is not used: an international application dated outside the year its number names (PCT/CCYYYY/; PCT/CCYY/ names its year up to 2003, and a later number written that way is read as 20YY when the row's date falls in that year; a number written without one or both slashes, such as PCTGB9300583, is read as PCT/GB93/00583) or before 1978-06-01, or whose number names no possible filing year (including a later PCT/CCYY/ number dated outside 20YY), or a US application dated outside the years in which applications of its serial number's series were filed; PatentRef also does not use a US parent date before 1950. The filing date another USPTO record PatentRef holds gives for the same application counts instead (another row of the same record, the international filing date in the record of its national stage, or the filing date in the record of the patent granted on it): US 9527819 counts from its parent PCT/US2005/039541, filed 2005-11-02 by the record of its national stage, not from the 2008-07-22 its own row gives. When none does and that parent could be the earliest, expiry_date_estimated is null, and expiry_status is the status the record's dates give on the date the row gives, when that date is a possible filing date, and on every filing date the parent's number allows (the year or the series years it names; any day from 1978-06-01 when it names no possible year or a serial number its office never reached in that year; never after the patent's own filing); when those give different statuses, expiry_status is unknown. Filed before 1995-06-08: 17 years from grant when that ends before 1995-06-08 or after the 20-year date, else 20 years from filing; expiry_basis names term_17y or term_20y, whichever applied (US 5000000, filed 1989, granted 1991: 20 years from filing, 2009-05-15, is later than 17 years from grant, so term_20y). Design patents: 14 years from grant, 15 when filed on or after 2015-05-13. A dated terminal disclaimer ends the term at its date. A national stage under 35 USC 371 counts from its international filing date (35 USC 363). A patent or reissue on the USPTO list of patent terms extended under 35 USC 156 (retrieved 2026-10-11, updated through 2026-09-08) takes the list's original expiration date plus the extension the list gives (expiry_basis pte_156:N). An extension only adds to the original term (35 USC 156(a)), so where the list's date is earlier than the term computed from the record and the record shows no terminal disclaimer, the two disagree: if only one of the dates has passed, expiry_date_estimated is null and expiry_status is unknown, or expired_fee_nonpayment when the fee record shows an unreinstated lapse whose grace period ran out before the later date. Not counted: a terminal disclaimer without a date (one that ties the term to another patent), an extension the list does not name (an interim extension, or one granted after the list's update date), and a change to the adjustment after grant. No estimate for a patent withdrawn from issue or for a reissue not on the list. A maintenance-fee lapse whose 6-month grace period ran out after the computed term end (the later of the two where the list and the record disagree) gives expiry_status expired_term, not expired_fee_nonpayment, and expiry_basis keeps fee_lapse:YYYY-MM-DD. - GET /lapse/status/{patent_id}/ (alias /status/{patent_id}/): one verdict record, {"patent_id", "status", "basis", "reinstatement_possible", "reinstatement_basis", "expiry_date_estimated", "lapse_date", "reinstated_date", "as_of", "data_version", "catalog_data_version", "permalink", "plain", "next_steps"}. patent_id as PatentsView prints it (10000000, D872010, RE31177; no US prefix). permalink is https://patentref.io/p/?v=; plain is one sentence for a person, built from the record facts, with as_of and the data version. The USPTO record prints dates and events, not a term end, so plain keeps the two apart: for active and expired_term it gives the term end computed from the dates in the record and says whether that date has passed or not yet ("As of 2026-10-06, the term end computed from the dates in the USPTO record for US 5000000 is 2009-05-15 (20 years from filing), a date that has passed, and the USPTO list of patent terms extended under 35 USC 156, updated through 2026-09-08, does not name this patent (data version 20261006.5)."); expired_term also says that the USPTO 35 USC 156 list does not name the patent, or names its reissue; for expired_fee_nonpayment it gives the date of the maintenance-fee record's expiry entry and says no reinstatement follows it; withdrawn says the record shows the patent as withdrawn from issue; unknown says the record does not settle the status because no term end can be computed from its dates, or, where the record gives a parent application a filing date it contradicts and PatentRef holds no other USPTO record that gives that application's filing date, names the application, the date and why the date does not fit its number. When the date the record gives (where it is a possible filing date) and every filing date the application's number allows give the same status, the active, expired_term and expired_fee_nonpayment sentences name the application, the date and why it does not fit, and say on which of those dates the status holds. For a patent on that list, active and expired_term give the list's original expiration date and extension and the term end they add up to, plus the record's own term end when that is later; when only one of the two has passed, the status is unknown and plain gives both, unless the fee record shows an unreinstated lapse whose grace period ran out before the later date: then the status is expired_fee_nonpayment with its sentence. next_steps lists links for a person and for an agent. reinstatement_basis names the rule behind reinstatement_possible: 37 CFR 1.378 for a maintenance-fee lapse, 35 USC 154 when the term has ended ("35 USC 154: term ended"; "35 USC 154: term ended (the lapse predates the term end)" for a fee lapse whose computed term end has since passed; "35 USC 154: term ended (the term ended before the fee lapse)" when the grace period of the unpaid fee ran out after the computed term end). The record has no "error" member, unlike the list and detail answers; branch on the status code. ?v=: a superseded version adds a "superseded" object (never a 404). Errors: a malformed id is 400 ERR_Q; an unknown patent is 404 {"detail": "Not found."} with no code header, as the detail routes; a version that never existed is 404 {"error": true} with X-Status-Reason-Code NOT_FOUND. - GET /lapse/similar?text=&n=<1 to 20, default 20>: the nearest patents by title and abstract, {"error", "data_version", "as_of", "method", "n", "terms", "neighbors": [{"rank", "patent_id", "patent_title", "patent_date", "score"}]}. FTS5 bm25 over the rarest input tokens (listed in terms). Usually under 2 s; the first queries after a data swap can take 10 s. The only path without a trailing slash; /lapse/similar/ answers too. - GET /lapse/similar?text=&n=<1 to 20>&mode=structured (experimental): the patents whose claims introduce the most of the text's term keys, {"error", "data_version", "text_data_version", "as_of", "method": "claim_term_keys", "mode": "structured", "structure_version", "generator", "n", "terms", "term_keys": [{"key", "weight"}], "key_phrases", "candidates": {"title_abstract", "key_phrases", "with_claim_text"}, "neighbors": [{"rank", "patent_id", "patent_title", "patent_date", "score", "shared_terms": [{"key", "match", "claim_key", "claim", "text", "start", "end"}]}]}. The keys are the noun phrases the claim tree's term finder reads in the text, as a claim tree's term key (lower case, the last word singular), weighed by ln(N / df) of their rarest word in titles and abstracts; keys whose rarest word is in more than 2 percent of patents are dropped unless fewer than 3 are left, at most 12 are kept (term_keys). Candidates come from the title and abstract index: the text mode's own query (terms) and the keys as phrases (key_phrases), 40 each. Each candidate's claims are parsed when asked for by the claim tree's parser (no language model, nothing stored); only the terms the claims introduce count. score is the weighed share of the text's keys found: 1 for the same key, half when one key ends with the other as whole words ("drive motor" and "electric drive motor"), so 1 means every key found; ties by the number of keys found the same, then patent_id. A patent with no key found, without claim text, or over the claim tree's work cap is left out, so neighbors can be fewer than n or empty. shared_terms says where each key is: match exact or partial, claim_key the claim's own key, claim the claim's number (the first independent claim that introduces it, else the first claim), and text, start and end in that claim's text, the offsets GET /structure/claims/{patent_id}/ serves. The answer carries X-Text-Data-Version. A score describes shared wording in the published text and nothing more. Experimental: the ranking is weakest where the shared terms are common short claim words, where near-identical patents tie, and where a text yields fewer than 3 term keys, as many chemistry abstracts do. Errors: a mode other than text or structured is 400 ERR_Q; with no long-text file in service mode=structured answers 501 ERR_NOT_IMPLEMENTED. Without mode, or with mode=text, the answer is the one above, unchanged. - https://patentref.io/p/?v=: the record page for a person (HTML, no key). When the data in service carries the patent family tables, the page also lists the patent's family outside the US: the publications outside the US that Google Patents Public Datasets puts in the same family (office, number, kind, publication date; the first 200), with the credit line "Family data from Google Patents Public Datasets, used under CC BY 4.0." and a link to the patent's family on USPTO Global Dossier. The legal status of those publications is not shown. PatentRef additions: claim trees (X-Api-Key, as every data path): - GET /structure/claims/{patent_id}/ (the trailing slash is optional): the claim tree of one granted patent, {"error": false, "patent_id", "patent_unit_id", "structure_version", "generator", "data_version", "text_data_version", "as_of", "kind", "utility_like", "claims": [...], "relations": [...], "flags": [...]}. It is built when it is asked for, from the claims the long-text file in service prints (the rows /g_claim/ answers for the same patent_id), by PatentRef's claim parser: regular expressions and small state machines, the same answer for the same text every time, no language model, nothing stored. patent_id as PatentsView prints it (10000000, D872010, RE31177, PP36776; a US prefix is read too). kind is utility, design, plant or reissue. The answer carries X-Data-Version and X-Text-Data-Version. - A claim: {"claim_id", "number", "number_basis", "sequence", "text", "body_start", "independent", "depends_on", "depends_on_basis", "depends_on_text", "depends_on_field", "dependent_field", "references", "category", "category_basis", "preamble", "transition", "elements", "terms", "claim_refs", "exemplary", "flags"}. text is the claim as published, untouched. depends_on lists the claims a claim refers back to in its preamble ("The method of claim 5"), or, when its text names none, the ones the dependent field of the long text names; depends_on_basis says which (text, field, field_body_reference or none), and depends_on_text and depends_on_field keep both readings. references lists the claims the text cites outside the preamble. category (method, apparatus, system, composition, crm for a computer-readable medium, use, design, plant or other) is read from the words of the preamble, or taken from the parent claim when a dependent claim's own preamble names none. preamble, transition and elements are the parts of the text around its transition word ("comprising", "consisting of", "wherein" and the like; transition.phrase is the word matched); an element's kind is element or wherein_clause. terms are noun phrase mentions with role introduces ("a widget") or refers ("the widget", "said widget"); a mention that refers carries antecedent, the term_id of the earlier mention it was matched to, with match (exact, head, trim or prefix, strongest first) and found_in (same_claim, dependency_chain or cited_claim: where the earlier mention was found), or antecedent null when none was found. claim_refs are the claim citations found in the text with the claims they name (targets). - Offsets: the preamble, the transition and every element, term mention and claim citation carry start and end, offsets into the claim's own text: text[start:end] is the part's text. They count Unicode code points (Python string indices); a UTF-16 client such as JavaScript converts before slicing text with characters outside the Basic Multilingual Plane. Every offset is checked on the server before the answer is sent. - relations: {"src", "rel", "dst"} between the ids of those parts: part_of, depends_on, references_claim, introduces_term, refers_to_term (a mention to its antecedent), has_flag. The ids: US for the patent, US-c for claim n (a repeated number gets -s), and -pre, -tr, -e, -t and -f after a claim id for its preamble, transition, elements, term mentions and flags. - flags: notes on the text and the data, each {"code", "flag_id", ...} with the details of its code, on a claim or on the patent. On a claim: dependent_field_text_mismatch (the dependent field and the preamble name different claims), dependency_reference_in_body (the dependent field names claims the text cites outside the preamble), dependent_field_unparsed, multiple_dependency (more than one parent claim), dependency_on_later_claim, dependency_on_missing_claim, self_dependency, dependency_cycle, claim_reference_outside_preamble, reference_to_missing_claim, preceding_claims_reference ("any preceding claim", read by position), claim_reference_letter_read_as_one ("claim l" read as claim 1), unparsed_claim_reference, empty_element, no_transition_found, empty_preamble, empty_claim_text, claim_number_text_mismatch, duplicate_claim_number, unmatched_reference (a mention that refers with no earlier mention found). On the patent: rows_reordered_by_claim_sequence, no_independent_claim, no_claims. A flag says what the parser saw in the text as printed and nothing more. - structure_version names the shape of the tree ("2") and goes up when the shape changes; generator names the parser and its version. It went from "1" to "2" in API 1.21.0, when the field of a term mention that says where its antecedent was found took the name found_in, with the same values; nothing else in a tree changed. Cache an answer under structure_version, data_version and text_data_version together. - Limits and errors: the tree is built when it is asked for, and the server keeps a bounded number of recent trees in memory. A claim set over 1,000 claim rows or 1,000,000 characters, or a read over 5 s, answers 500 ERR_ES (the largest claim set in the data has 887 claims). A string that is not a patent number is 400 ERR_Q; a patent the data in service does not have, and a patent it has without claim text in the long-text file (granted after that file's last grant date), answer 404 {"error": true} with X-Status-Reason-Code NOT_FOUND and X-Status-Reason saying which; with no long-text file in service the path answers 501 ERR_NOT_IMPLEMENTED. - Example: GET /structure/claims/10034028/ with X-Api-Key. Its claim 6, abridged (one of its three term mentions): {"claim_id": "US10034028-c6", "number": 6, "text": "6. The method of claim 5 , wherein the caption stream is associated with the media stream.", "independent": false, "depends_on": [5], "depends_on_basis": "text", "category": "method", "preamble": {"unit_id": "US10034028-c6-pre", "text": "The method of claim 5", "start": 3, "end": 24}, "transition": {"unit_id": "US10034028-c6-tr", "text": "wherein", "start": 27, "end": 34, "phrase": "wherein"}, "elements": [{"element_id": "US10034028-c6-e1", "text": "the caption stream is associated with the media stream", "start": 35, "end": 89, "kind": "wherein_clause"}], "terms": [{"term_id": "US10034028-c6-t2", "text": "the caption stream", "start": 35, "end": 53, "role": "refers", "antecedent": "US10034028-c5-t18", "match": "exact", "found_in": "dependency_chain"}], "flags": []} PatentRef additions: patent briefs and links to claims (the brief needs X-Api-Key, as every data path; the record page needs none): - GET /structure/brief/{patent_id}/?max_tokens=<300 to 32000, default 2000> (the trailing slash is optional): a brief of one granted patent that fits max_tokens, {"error": false, "patent_id", "structure_version", "generator", "data_version", "text_data_version", "as_of", "max_tokens", "tokens", "tokenizer", "truncated", "omitted": {"independent_claims", "elements", "dependent_claims", "component_keys"}, "page", "identity", "status", "independent_claims", "dependent_claims", "component_keys"}. It is built when it is asked for, from the patent's record, its status record and its claim tree (the claim tree's parser: no language model, nothing stored). patent_id as PatentsView prints it (a US prefix is read too). The answer carries X-Data-Version and X-Text-Data-Version. - What it holds, in this order: identity {"patent_id", "patent_title", "patent_type", "patent_date", "filing_date", "claim_count", "independent_count"} (patent_date is the grant); status, the record GET /lapse/status/{patent_id}/ answers without its links, {"status", "basis", "expiry_date_estimated", "lapse_date", "reinstated_date", "reinstatement_possible", "reinstatement_basis", "plain", "as_of"} (null when no status catalog is in service); independent_claims [{"number", "anchor", "category", "preamble", "transition", "element_count", "elements": [{"anchor", "start", "end", "text", "kind"}]}] (preamble and transition are quotes or null; kind only on a wherein_clause); dependent_claims, one line each, [{"number", "anchor", "depends_on", "start", "end", "text", "cut"}]: the claim's text from its transition, or from its body when it has none, to its end, cut at a space after at most 240 characters (cut: true, only then); component_keys [{"key", "claims"}]: the term key of each term the claims introduce (a claim tree's key: the patent's words, lower case, the last word singular) with the numbers of the claims that introduce it, ordered by the first claim that introduces it, independent claims first, then by offset. - The cut: the items go in in that order (identity, status, each independent claim's head and then its elements one by one, each dependent line, each key), and the first item that does not fit ends the brief, so the brief for a smaller budget is the start of the brief for a larger one. truncated says whether the budget left anything out and omitted counts what it left out. tokens is the whole answer's size by the stated proxy that tokenizer names, "ceil(utf8_bytes / 3)": the UTF-8 bytes of the JSON body as sent, divided by 3 and rounded up; it is never over max_tokens. A model's own tokenizer counts differently: leave room by asking for less. - Quotes and offsets: a preamble, a transition, every element and every dependent line carry start and end, offsets into the claim's own text as published (text[start:end] is the quote, in Unicode code points, the offsets GET /structure/claims/{patent_id}/ serves), and every one is checked on the server before the answer is sent. The title, the quoted text and the keys are the patent's own words, as printed. Nothing in a brief is a conclusion about a claim. - Links to claims and elements: the record page https://patentref.io/p/ shows the patent's claims below its record, each claim's text up to its first element and then one line for each element, and gives each claim and each element an anchor: c for claim n (c-s when a number repeats in the data) and c-e-- for element k of claim n, with its start and end in the claim text. The page plus "#" plus an anchor opens that claim or element, for example https://patentref.io/p/10034028#c1-e4-462-602 (a brief's page names the data version too). The same data gives the same anchors on every build. When the data changes and an anchor is no longer on the page, a link to it opens the claim it names, with a note saying so; when the page has no such claim, the claims heading, with the note. - Limits and errors: a string that is not a patent number, and a max_tokens that is not a whole number from 300 to 32000, are 400 ERR_Q; a patent the data in service does not have, and one without claim text in the long-text file (granted after that file's last grant date), answer 404 {"error": true} with X-Status-Reason-Code NOT_FOUND and X-Status-Reason saying which; a claim set over the claim tree's work cap answers 500 ERR_ES; with no long-text file in service the path answers 501 ERR_NOT_IMPLEMENTED; 503 ERR_ES when the status records cannot be read. The server keeps a bounded number of recent briefs in memory. - Example: GET /structure/brief/10034028/?max_tokens=1000 with X-Api-Key. Abridged (identity, status and four of the six elements it holds left out here): {"error": false, "patent_id": "10034028", "max_tokens": 1000, "tokens": 963, "tokenizer": "ceil(utf8_bytes / 3)", "truncated": true, "omitted": {"independent_claims": 2, "elements": 17, "dependent_claims": 15, "component_keys": 53}, "page": "https://patentref.io/p/10034028?v=20261027.2", "independent_claims": [{"number": 1, "anchor": "c1", "category": "method", "preamble": {"start": 3, "end": 78, "text": "A method in a computer system for caption synchronization of media programs"}, "transition": {"start": 79, "end": 89, "text": "comprising"}, "element_count": 11, "elements": [{"anchor": "c1-e1-90-211", "start": 90, "end": 211, "text": "receiving at least one portion of a media stream and at least one portion of a caption data stream in the computer system"}, {"anchor": "c1-e2-213-344", "start": 213, "end": 344, "text": "wherein the at least one portion of the media stream and the at least one portion of the caption data stream correspond to an event", "kind": "wherein_clause"}]}], "dependent_claims": [], "component_keys": []} PatentRef additions: claim counts (X-Api-Key, as every data path): - GET /structure/counts/{patent_id}/ (the trailing slash is optional): the claim counts of one granted patent, {"error": false, "patent_id", "patent_unit_id", "counts_version", "structure_version", "generator", "data_version", "text_data_version", "as_of", "counts": {...}, "claim_counts": [...]}, in the spirit of the USPTO Patent Claims Research Dataset (the USPTO Office of the Chief Economist's claims data for grants of 1976 to 2014), for any grant the long-text file holds. They are counted when asked for, from the claim tree /structure/claims/ builds from the same claims: no language model, nothing stored. The answer carries X-Data-Version and X-Text-Data-Version. - counts, in this order (the columns of the claim counts snapshot after patent_id): {"claims", "independent_claims", "dependent_claims", "claims_citing_other_claims", "multiple_dependent_claims", "cancelled_claims", "words", "words_first_claim", "words_independent_total", "words_independent_min", "words_independent_mean", "words_independent_max", "words_dependent_total", "words_dependent_min", "words_dependent_mean", "characters", "elements_independent_min", "elements_independent_mean", "elements_independent_max", "dependency_depth_max", "consisting_words", "or_words", "selected_from_phrases"}. Every field is a count, or the least, mean (to two decimals) or most of counts; a least, mean or most is null when the patent has no claim of that kind. - claim_counts: one entry per claim, in claim order: {"number", "sequence", "words", "characters", "elements", "wherein_clauses", "parent_claims", "cited_claims", "dependency_depth", "consisting_words", "or_words", "selected_from_phrases", "cancelled"}. - The rules: independent_claims and dependent_claims follow the claim tree, so a claim that names another claim only in its body ("a system that performs the method of claim 1") stays independent; claims_citing_other_claims follows the USPTO dataset's rule (a claim whose text names another claim anywhere is dependent), so the USPTO's independent count is claims minus claims_citing_other_claims. Words and characters are counted after a claim's number prefix. A word is a run of characters between white space with at least one letter or digit, so a comma or bracket the long text sets off with spaces is not a word; characters count each run of white space as one space and no space before a comma, semicolon, colon, full stop or closing bracket or after an opening bracket. A claim whose whole text is a cancellation note ("(canceled)") counts only in cancelled_claims. dependency_depth is 0 for an independent claim and 1 more than the deepest claim it depends on. consisting_words, or_words and selected_from_phrases count those words as the USPTO dataset's cns_ct, or_ct and sf_ct do. Each field and the USPTO variable it follows: https://patentref.io/docs/claim-counts.html and the ClaimCountsPatent and ClaimCountsClaim schemas of openapi.json. - Checked against the USPTO files (BigQuery patents-public-data.uspto_oce_claims) on 1,000 random grants of 1976 to 2014, of which the files hold 898 (they hold no design or plant patents): the claims of a patent are equal for 99.8 percent of patents; the USPTO rule's independent and dependent counts are equal for 98.9 and 99.1 percent of patents; a claim's words are within 2 words or 2 percent of the USPTO count for 93.0 percent of claims (97.7 percent for grants from 2002), and the shortest independent claim is within 2 words or 5 percent for 92.1 percent of patents. For grants from 2002 on the USPTO files count a claim's number ("1.") as a word when their claim text starts with it, and the comparison takes that word off their counts first; their texts of 1976 to 2001 often run two words together at a comma, semicolon or colon, so their word counts for those years run lower than these. - counts_version names the fields and their rules ("1") and goes up when either changes. Cache an answer under counts_version, structure_version, data_version and text_data_version together. - Limits and errors are the claim tree's: a claim set over 1,000 claim rows or 1,000,000 characters, or a read over 5 s, answers 500 ERR_ES; a string that is not a patent number is 400 ERR_Q; a patent the data in service does not have, and one without claim text in the long-text file, answer 404 {"error": true} with X-Status-Reason-Code NOT_FOUND and X-Status-Reason saying which; with no long-text file in service the path answers 501 ERR_NOT_IMPLEMENTED. - Example: GET /structure/counts/10034028/ with X-Api-Key. Its counts: {"claims": 18, "independent_claims": 3, "dependent_claims": 15, "claims_citing_other_claims": 16, "multiple_dependent_claims": 0, "cancelled_claims": 0, "words": 1245, "words_first_claim": 262, "words_independent_total": 575, "words_independent_min": 153, "words_independent_mean": 191.67, "words_independent_max": 262, "words_dependent_total": 670, "words_dependent_min": 15, "words_dependent_mean": 44.67, "characters": 7381, "elements_independent_min": 4, "elements_independent_mean": 5.33, "elements_independent_max": 8, "dependency_depth_max": 4, "consisting_words": 0, "or_words": 13, "selected_from_phrases": 0} PatentRef additions: the triage table (X-Api-Key, as every data path): - POST /structure/triage/ (GET takes the same fields as parameters; the trailing slash is optional): {"patents": [...] or ""} or {"search": "", "limit": <1 to 500, default 100>}, with "components" (optional) and "output" ("json", the default, or "csv"). The answer: {"error": false, "data_version", "text_data_version", "as_of", "structure_version", "generator", "input", "components": [{"key", "from", "asked", "patents"}], "positions", "complete", "time_limit_seconds", "status_available", "counts": {"patents", "found", "built", "not_built"}, "quotes", "export_columns", "patents": [...]}. It carries X-Data-Version and X-Text-Data-Version. A list of 500 numbers is too long for a GET request line: send it by POST. - The patents: a list of up to 500 numbers (a JSON list, or text with the numbers separated by spaces, commas, semicolons or new lines; US, thousands separators such as 10,905,426, hyphens and kind codes such as B2 are read, and pieces that are not patent numbers are listed in input.unread), in the order given, each once; or a search: the claims that hold every word of search (at most 12 words and "quoted phrases"), read from the full-text index of the claims in the reverse of the order the long-text file loaded them (the bulk files year by year, oldest first, so recent grants tend to come first), each patent once, the first limit of them. input.more says more patents match; input.complete is false when the search stopped at its 5 s. - A row: {"row", "patent_id", "title", "patent_date", "permalink", "found", "built", "note", "status", "claims": {"total", "independent"}, "cells", "components", "claim_parts"}. status holds status, basis, reinstatement_possible, reinstatement_basis, expiry_date_estimated, lapse_date, reinstated_date and as_of: the values GET /lapse/status/{patent_id}/ gives (null for a number the data in service does not have). note says why a row has no cells: not_in_data, no_claim_text (granted after the long-text file's last grant date), work_cap (over the claim tree's 1,000 claim rows or 1,000,000 characters), offset_check (a span that did not hold) or time_limit (below). - Components: up to 12 in components, each read as a claim tree's term key (the noun phrase the term finder reads, lower case, the last word singular: "drive motors" is drive motor; words it does not read as a noun phrase stay as written); without any, the table picks up to 6: the terms the most of its patents introduce, each in at least two of them, weighed by ln(N / df) of the term's rarest word in titles and abstracts. components[].from is request or table, and patents counts the table's patents whose claims mention it. - A cell, one per component in the table's order: {"key", "position", "match", "claim_keys", "independent_claims", "dependent_claims", "quote"}. position: every_independent (each independent claim mentions the component), some_independent (some independent claims do), only_dependent (no independent claim does, and a dependent claim does) or absent (no claim does). A claim mentions a component when one of its term mentions (an "a" or "the" phrase of the claim tree) has the component's key, or refers back to a mention that has it ("the device" matched to "an electronic device"). match is exact (the same key) or partial (one key ends with the other as whole words: drive motor and electric drive motor), and claim_keys names the claim terms' keys it matched. quote is the first mention, in the first independent claim that has one, else the first claim: {"claim", "claim_id", "term_id", "start", "end", "text"}. - A row's components: the keys of the terms its own claims introduce, grouped as every_independent, some_independent and only_dependent. claim_parts: each independent claim as {"claim", "claim_id", "category", "parts": [{"part", "claim", "claim_id", "part_id", "start", "end", "text"}]}, part preamble, transition, element or wherein_clause in the order of the claim's text (an element can add label, its own label in the text, and opens_list); part_id is the part's id in the claim tree of GET /structure/claims/{patent_id}/. - Quotes: text is the claim's text from start to end (Unicode code points, as in the claim tree), and the server checks every quote against the claim text before the answer leaves; quotes counts them. - output=csv: the export, RFC 4180 (comma, CRLF, a field quoted when it holds a quote, a comma or a line break), UTF-8 without a byte order mark, the columns of export_columns: patent_id, title, status, status_basis (the basis codes joined by semicolons), reinstatement_possible, as_of, note, row_kind (patent, component, preamble, transition, element or wherein_clause), component, position, match, claim, claim_id, part_id, label, quote, start, end, mark. One row per patent, one per component of each row with claims, and one per claim part. The mark column is empty here: have and lacks are the reader's own notes, which the page https://patentref.io/triage/ keeps in the browser and writes into its own export (the same columns); the server never receives a mark. - Limits and errors: one table at a time per key, and a few at once on the server; beyond that 429 with Retry-After and an X-Status-Reason that says so. A table stops reading claims after 20 s: the rows it did not reach say note time_limit and complete is false; send them again (trees a server process already read come back faster). One request toward the monthly allowance, whatever the number of patents. A request the table does not read is 400 ERR_Q, X-Status-Reason saying which field; with no long-text file in service the path answers 501 ERR_NOT_IMPLEMENTED, as a search does when that file has no full-text index of the claims. The table describes the text of the claims and nothing more. PatentRef additions: the rules layer (X-Api-Key, as every data path; office is US, EP, CN, JP, KR, AU, or PCT for the international phase): - Every answer: {"error": false, "tool", "query", "rules_version", "as_of", "count", "rows": [...], "note"}. A row: {"id", "office", "rule_kind", "act", "rule_text", "value", "effective_from", "effective_to", "superseded_by", "citation_url", "citation_quote", "extra_quotes", "written_by", "verified_on", "verified_by", "verification_method", "version", "notes"}. citation_url is the official page the row was read from (the office's own page, the statute or regulation, or the WIPO PCT Applicant's Guide national chapter); citation_quote is verbatim from it, under 300 characters. effective_from 1900-01-01 means the cited source states no start. Records of what the rules say, not legal advice. - GET /rules/: the version, as_of, the offices with their row counts per rule_kind, the tools with their parameters, and the act vocabulary per rule_kind. - GET /rules/rep_required/?office=&act=: representation rows (when an applicant must act through a registered agent). GET /rules/fee_schedule/?office=&as_of=YYYY-MM-DD: the fee rows in force on as_of (default today, UTC), amounts with their currency. GET /rules/route_windows/?office=: generic windows (months from a named event such as the priority date). GET /rules/pph_routes/?from_office=&to_office=: the PPH rows of to_office whose source names from_office as a partner. GET /rules/ai_use_statement_required/?office=: an empty answer means no cited rule, and nothing is inferred from it. GET /rules/grace_period/?office=: citations only. GET /rules/changelog/?since=x.y.z or YYYY-MM-DD: the releases after it. - No rules path takes an application number, a patent number or a priority date (such a parameter answers 400 ERR_Q), and none computes a date for an application. An unknown parameter or a value out of its list answers 400 ERR_Q with the reason in X-Status-Reason. PatentRef additions: build facts, health, status, accounts (no key needed unless stated): - GET /meta/: {"service", "base", "data_version", "build_id", "release", "as_of", "refreshed_at", "built_at", "snapshot_sha256", "index_set", "previous_version", "superseded_versions", "delta": [{"table", "current", "next", "added", "removed", "verdict"}], "delta_note", "fee", "weekly_grants", "deferred", "rules_version", "attribution", "licenses_url", "guide", "openapi", "generated_at"}. rules_version: the version of the rules dataset in service (null when it cannot be read). superseded_versions: builds that were ready and are not the one in service, in version order; a newer build waiting for its swap is never listed, and a build that served and was rolled back is. A permalink with ?v= one of them shows the superseded banner. delta: rows per table of this build against the one it replaced. The names come from the build step, which compared the build then in service with the new one: "current" is the build this one replaced and "next" is this build, the one in service now. verdict is the count rule on the PatentsView rows of the table (equal, +N.NN% within the growth limit, GREW>limit, SHRANK, new table, missing), followed after a semicolon by the rows the weekly grants added: "equal; grants +286,939" on patents means the PatentsView rows did not change and the weekly grants added 286,939. fee: the build's maintenance-fee delta, {"current", "next", "flags"}, where current and next (same naming) each carry {"lapsed", "max_lapse_date", "events", "file"}, or null. weekly_grants: the weekly grants the file carries beyond the PatentsView release, or null. deferred: the data sets the files in service lack, read from those files, one string each: the name, a colon, the routes that answer 501 without it and, for a classification lookup, where each patent carries the same facts ("wipo: /api/v1/wipo/ (each patent's WIPO fields come in its wipo group)"); empty when nothing is missing. - GET /meta/health/: {"ok", "version", "data_version", "db", "time", "text_data_version", "patentref_version"}; 200 when the database answers, else 503; HEAD answers the same status with no body. version and patentref_version are the commits of the server code. - GET /meta/status/ (HTML) and /meta/status.json: the public status figures, {"generated_at", "cache_seconds", "state", "service", "uptime", "external_check", "uptime_official", "requests_24h", "requests_30d", "refresh", "box", "alerts"}, cached 60 s. state is ["ok" or "degraded" or "down", a sentence]; uptime is the GitHub probe's record; external_check is UptimeRobot's check of /meta/health/ from its own network (state, state_since, last_check_at, interval_minutes, incidents, fresh); uptime_official is the uptime figure since 2026-10-06 03:47 UTC, the GitHub probe's down time until 2026-10-15 02:57:08 UTC and UptimeRobot's incidents after it (uptime_percent, down_minutes, switch_at, note); requests_24h and requests_30d count the answers on the data paths under /api/v1/ (requests, ok, client_errors, rejected for 403 and 429, server_errors, not_served, p50_ms, p95_ms, max_ms, error_rate, server_error_rate): not_served is the documented 501 of an endpoint whose data set is not in this release (the deferred list of /api/v1/meta/) and is not a server error, and the requests the service sends to check itself are left out of the figures and counted in own_requests; refresh.current and refresh.next are the build in service and the next build with their gates; box shows the free share of the system and data disks and of memory with an ok flag, the last backup and the watchdog checks. No server paths, internal alert ids or disk and memory sizes appear in it. - GET /meta/signup/, GET and POST /meta/keys/ (bearer), GET /meta/usage/ (key or bearer): section 1. - https://patentref.io/LICENSES.md: every table, source and quote with its license and attribution line. ## 5. Errors Every error keeps the upstream form: the body {"error": true} (a 403 without a key, a 429 and a detail 404 carry DRF's {"detail": "..."} instead) and, on 400, 500 and 501 (and on the 404 of a v that never existed on /lapse/status/ and the 404 of /structure/claims/, /structure/brief/ and /structure/counts/), two headers: - X-Status-Reason: the reason in upstream English, for example Invalid JSON in 'q' parameter. - X-Status-Reason-Code: ERR_Q (bad request, 400), ERR_ES (search error or timeout, 500) and ERR_NOT_IMPLEMENTED (data set not in this snapshot, 501) on the data endpoints; NOT_FOUND (404) on /lapse/status// when v names a version that never existed, and on /structure/claims// for a patent without a claim tree (not in the data, or without claim text), as on /structure/brief// and /structure/counts//; on the account endpoints under /meta/ only: ERR_KEY (no or revoked key, 403), ERR_AUTH (bad bearer token, 401), ERR_SIGNUP_LIMIT (sign-up cap, 429), ERR_TERMS (a key mint or rotation without the terms accepted, 403). A data endpoint's 403 (no key) and 429 (throttled) and a detail route's 404 carry no code header, as upstream. Every code, with what to do, is at https://patentref.io/docs/errors/ (one page each at /docs/errors/). The problem details "code" member uses the header value when there is one, and otherwise ERR_KEY (403), THROTTLED (429) or NOT_FOUND (404). Content negotiation (RFC 9457): send Accept: application/problem+json and any error comes back as a problem details object with Content-Type: application/problem+json and the two headers unchanged: {"type": "https://patentref.io/docs/errors/ERR_Q", "title": "Bad Request", "status": 400, "instance": "/api/v1/patent/", "detail": "Invalid field: nope", "code": "ERR_Q", "data_version": "20261006.4"} Without that Accept value the bodies are byte for byte what upstream returned, so clients that branch on error: true keep working. Status codes: 200 ok; 301 a detail route without its trailing slash (section 2); 400 bad query (ERR_Q), or a GET request line over 4,094 bytes (an HTML page from the web server in front of the API, no code header); 401 bad bearer token (account endpoints, ERR_AUTH); 403 no key (no code header on data endpoints; problem details code ERR_KEY), or a key mint without the terms accepted (ERR_TERMS); 404 unknown detail key (problem details code NOT_FOUND); 413 a request body over 64,000 bytes (from the web server in front of the API: an empty body, no code header, with or without a key); 429 throttled (Retry-After; problem details code THROTTLED); 500 search error or timeout (ERR_ES); 501 data set not in this snapshot (ERR_NOT_IMPLEMENTED); 503 health check failing. ## 6. Data - Source: USPTO PatentsView bulk data (granted patents 1976 to the current release; disambiguated inventors, assignees, locations and attorneys; citations; classifications; maintenance-fee events from the USPTO Open Data Portal). Used and redistributed under CC BY 4.0; attribution: "Patent data from PatentsView and the USPTO, used under CC BY 4.0." PatentRef is an independent service and is not affiliated with the USPTO. - Refresh: the USPTO catalog is polled weekly; a new release is built, checked (integrity, row-count and fee-value deltas, the contract examples) and swapped in without downtime. data_version changes with every build; GET /meta/ lists the delta. - Weekly grants: PatentsView releases lag the grants by months, so every Tuesday's grants are added from the USPTO weekly grant full text (Open Data Portal product PTGRXML) within days of issue, for the patents the PatentsView release does not cover yet. Their inventor, assignee and attorney names are as printed, with no inventor_id, assignee_id or attorney_id; what derives from those ids is null too (the links to the entity records, inventor_location_id and assignee_location_id, inventor_gender_code). They carry "disambiguated": false when f names inventors.disambiguated (assignees.disambiguated and attorneys.disambiguated alike); then every row of that group carries the field, true on PatentsView rows. A whole group (f names inventors) carries the upstream fields only, on both kinds of rows, so every row of a group has the same keys. Find them with q {"inventors.disambiguated": false}. When a PatentsView release covers those patents, its rows replace the weekly ones. GET /meta/ shows weekly_grants: the last date PatentsView covers and the last weekly issue loaded. - Assignee names as printed: assignee_organization, assignee_individual_name_first and assignee_individual_name_last on /patent/ and /assignee/ are PatentsView's disambiguated names, one label for a whole cluster of spellings, which can differ from the name printed on a grant (US 6859933 is printed "Samsung Electronics, Co., Ltd."; its cluster label is "SAMSUNG DISPLAY CO., LTD."). The names as printed come beside them when f names them: assignees.raw_assignee_organization, assignees.raw_assignee_individual_name_first and assignees.raw_assignee_individual_name_last on /patent/ (then on every row of the group; a whole group in f carries the upstream fields only), and raw_assignee_organization, raw_assignee_individual_name_first and raw_assignee_individual_name_last on /assignee/ (the names printed on the assignee's most recent patent, the patent its other names come from). They come from PatentsView's g_assignee_not_disambiguated file and, for weekly grants, from the grant full text. They cannot be used in q or s (400 ERR_Q). They are null where the bulk data has none and in a data version built before them. - Snapshot: the data behind the API as three SQLite files (the search file, the Lapse catalog and the long text), on public object storage with a Zenodo DOI, 10.5281/zenodo.23250455 (it resolves to the newest version; one version a quarter); the files, their SHA-256 and the download links are at https://patentref.io/docs/snapshot.html. A snapshot is one fixed build; the API's data version moves on every week. - Withdrawn patents are excluded from /patent/ by default (o.exclude_withdrawn true), as upstream; set it to false to include them. - Known differences from the upstream Elasticsearch service: text search tokenizes with FTS5 unicode61 (case folded, punctuation and hyphens separate tokens, accents kept, no stemming; apostrophes and dotted numbers can split differently); ties in a sort are broken by the row order of the snapshot; prefix and wildcard on analyzed text fields test tokens; _contains, a range or an empty _begins on a long-text field needs patent_id in the same _and (400 ERR_Q otherwise, section 4), where upstream's Elasticsearch answered from its term dictionary. Each difference is listed in the server's change file, https://github.com/Nietzsche247/PatentSearch-API/blob/sqlite-backend/CHANGES_LAPSE.md. - Known differences in paths, limits and order: /api/v1/patent/otherreference/ answers as /api/v1/patent/other_reference/ does (upstream's release 2.2.0 of 2024-11-06 moved it to other_reference, and upstream's code routes only that path since; PatentRef answers both, so every path in the endpoint table of the PatentsView code examples notebook answers as written). A request body over 64,000 bytes answers 413, and q over 16 KB 400 ERR_Q (section 2), limits upstream did not have. The rows of a nested group come in the order of their source (section 3), which can differ from the order upstream's Elasticsearch returned. - Nothing PatentRef returns is a legal conclusion. Expiry and fee status (the lapse group and /lapse/status/, section 4) are record facts with their basis and an as-of date. ## 7. Worked examples (all with X-Api-Key) 1. One patent with inventors and assignees: GET /api/v1/patent/?q={"patent_id":"10905426"}&f=["patent_id","patent_title","patent_date","inventors.inventor_name_first","inventors.inventor_name_last","assignees.assignee_organization"] 2. Patents of an organization since 2020, newest first, 200 a page: GET /api/v1/patent/?q={"_and":[{"_begins":{"assignees.assignee_organization":"Apple"}},{"_gte":{"patent_date":"2020-01-01"}}]}&s=[{"patent_date":"desc"},{"patent_id":"asc"}]&o={"size":200} next page: o={"size":200,"after":["",""]} 3. Phrase search in titles, counting only: GET /api/v1/patent/?q={"_text_phrase":{"patent_title":"solid state battery"}}&o={"size":1} then read total_hits. 4. Forward citations of a patent: GET /api/v1/patent/us_patent_citation/?q={"citation_patent_id":"7861317"}&f=["patent_id","citation_date"] 5. An inventor's patents by year: GET /api/v1/inventor/?q={"inventor_name_last":"Hopper"}&f=["inventor_id","inventor_name_first","inventor_name_last","inventor_years"] 6. Patents in a CPC group: GET /api/v1/patent/?q={"cpc_current.cpc_group_id":"H01M10/0525"}&f=["patent_id","patent_title"] 7. Problem details on an error: curl -G -H "X-Api-Key: $KEY" -H "Accept: application/problem+json" https://patentref.io/api/v1/patent/ --data-urlencode 'q={"not_a_field":"x"}' 8. Is a patent expired, and can it be revived: GET /api/v1/lapse/status/6671888/ (status expired_term, basis ["term_20y", "pta_days:129", "reinstated:2017-08-30"], reinstatement_possible false: the term end computed from its dates, 2022-08-08, has passed) 9. Expiry facts for the results of a search: GET /api/v1/patent/?q={"_text_phrase":{"patent_title":"solid state battery"}}&f=["patent_id","patent_title","lapse.expiry_status","lapse.expiry_date_estimated","lapse.lapse_date"] 10. The claims of a patent, claim 1 first: GET /api/v1/g_claim/?q={"patent_id":"10000000"}&f=["claim_sequence","claim_number","claim_dependent","claim_text"] 11. The assignee as printed on the grant, beside PatentsView's disambiguated name: GET /api/v1/patent/?q={"patent_id":"6859933"}&f=["patent_id","assignees.assignee_organization","assignees.raw_assignee_organization"] (assignees [{"assignee_organization": "SAMSUNG DISPLAY CO., LTD.", "raw_assignee_organization": "Samsung Electronics, Co., Ltd."}]; null in place of the printed name in a data version built before it) ## 8. Good citizenship Stay under 45 requests a minute per key; back off on 429 for the Retry-After seconds. Page with o.after and size 1000 rather than many small pages. Ask for the fields you need in f (the default is three fields). Cache by X-Data-Version. One account, one key, rotated when leaked.