Pinakel · Trademark Data API · v1
Integrator Guide
Structured trademark search, lookup and change tracking across national and regional registers, built for docketing systems and IP platforms. Base URL:
https://api.pinakelai.com
Interactive OpenAPI reference: /docs · machine-readable spec: /openapi.json
Authentication
Every data request carries your API key in the X-API-Key
header (or as a Bearer token). Keys are issued when your
agreement is set up and can be viewed, rotated and monitored in your
customer portal.
curl -H "X-API-Key: pk_live_…" \
"https://api.pinakelai.com/v1/trademarks?application_number=2078049"
Identity: jurisdiction_code, office, uid
(jurisdiction_code, uid) is the stable identity of every
record — uid is opaque text (do not assume a format) and is
unique within a jurisdiction. jurisdiction_code is the
register the record comes from. office is the registering
office as the source record states it, and legitimately differs for
international records: a Madrid-protocol mark inside the Swiss register has
jurisdiction_code: "CH" and office: "WO".
Endpoints
GET/v1/trademarks
Global structured search across every covered jurisdiction. All filter
parameters are AND-combined; values inside one comma-separated parameter are
OR. At least one filter is required. Responses are an envelope:
{"items": […], "next_cursor": "…"|null} — an empty
items with HTTP 200 means "no matches", which is an answer, not
an error.
GET /v1/trademarks?owner_name_contains=lululemon&jurisdiction=CA,US,EM
GET /v1/trademarks?status=Registered&expiry_date_from=2026-10-01&expiry_date_to=2026-12-31
GET /v1/trademarks?application_number=2078049
GET/v1/{jurisdiction_code}/trademarks
The same search grammar scoped to one register (drop the
jurisdiction parameter — the path pins it).
GET /v1/CA/trademarks?registration_number=TMA1164763
GET /v1/CA/trademarks?mark_name_contains=zenith&nice_class=25,35
Search parameters
| parameter | matches | semantics |
|---|---|---|
| application_number registration_number | office numbers | exact. Several offices reuse numbers across series, so expect a list |
| jurisdiction | register | CSV of codes, up to 20 (global route only) |
| office | registering office | CSV — e.g. office=WO&jurisdiction=CH = Madrid designations inside the Swiss register |
| status | normalized status | CSV of Registered · Pending · Abandoned · Cancelled · Unknown (case-insensitive) |
| mark_feature | mark type | CSV — Word, Figurative, Combined, Three-dimensional, … |
| kind_of_mark | kind | CSV — Individual, Collective, Certificate, … |
| nice_class | Nice classes | CSV of integers; matches records in any listed class |
| nice_class_all | Nice classes | record must carry all listed classes |
| vienna_code | figurative elements | CSV; full code (27.05.01) or hierarchical prefix (27.05, 27 — prefix needs a companion filter) |
| mark_name | mark text | exact, case-insensitive; also matches translations/transliterations we hold |
| mark_name_startswith mark_name_contains | mark text | prefix / substring, case-insensitive, min 3 chars; requires a jurisdiction scope of ≤10 codes |
| owner_name (+_startswith, _contains) | applicant / owner / holder | one parameter covers all proprietor roles — offices disagree on where the current owner is recorded |
| representative_name (+_startswith, _contains) | representative / agent / correspondent of record | search a firm's own portfolio. Merged across roles: some offices (e.g. the US) record the responsible attorney only as the correspondent. Note: self-filed marks may list the owner as their own correspondent |
| owner_country | proprietor country | CSV of ISO codes |
| international_registration_number | Madrid IR number | exact — finds the national parts of one international registration across registers |
| priority_number | priority claim number | exact — links a family across offices via its shared priority claim |
| designated_country | Madrid designations | CSV of ISO codes — records designating any listed country |
| application_date registration_date publication_date status_date expiry_date termination_date opposition_period_end | lifecycle dates | exact: field=YYYY-MM-DD · range: field_from= and/or field_to= (inclusive, either side may be open) |
| updated_since updated_before | Pinakel change time | ISO timestamps, half-open interval [since, before) |
| sort, order | — | sort: updated_at (default) · application_date · registration_date; order: asc | desc (default) |
| limit, cursor | — | limit ≤500 (≤50 with expand=payload); pass back next_cursor verbatim — it is bound to the exact query that produced it |
| expand=payload | — | attach the full normalized document to every item |
| count=capped | — | adds matched_count (capped at 10,000) + matched_count_capped. Exact totals over the corpus are deliberately not offered |
query_too_broad (400) or stopped with query_timeout
(422) after 10 s — both tell you what to narrow. Unknown parameters are
rejected, never ignored; q is reserved for a future query
language.GET/v1/{jurisdiction_code}/trademarks/{uid}
Canonical fetch. Store (jurisdiction_code, uid) after the
first lookup and use it for everything afterwards.
?expand=payload attaches the full document.
GET/v1/{jurisdiction_code}/changes?since=…
· GET/v1/changes?since=…
The docketing sync feed: every record whose content changed at or
after since, in change order, keyset-paginated. The global feed
covers all jurisdictions in one loop; the scoped feed covers one.
GET /v1/CA/changes?since=2026-08-01&limit=200&expand=payload
GET /v1/changes?since=2026-08-01&cursor=eyJ…&limit=5000
The contract, precisely:
- The cursor is a durable frontier. Persist
next_cursorand resume from it in any later session (with the samesince); you will see every subsequent change exactly once. - Upsert by
(jurisdiction_code, uid). Only real content changes are delivered — a reload of identical source data on our side is not re-delivered.first_seen_at == updated_atidentifies records new to Pinakel;first_seen_at < updated_atidentifies updates. - Every non-empty page carries
next_cursor— persist the latest one. An empty page (items: [],next_cursor: null) means caught up to the safety horizon (the feed intentionally stops 5 minutes short of now) — keep your stored cursor and poll again later. expand=payloadattaches full documents (limit ≤200 in that mode) — one sync loop, no per-record detail calls.
GET/v1/jurisdictions
Every register the API currently serves, with record counts and the daily-pipeline frontier — the machine-readable version of the coverage table below.
GET/v1/usage
Your key's own month-to-date call count, quota and rate limit — poll it freely to watch your consumption.
Response fields & null semantics
Searches and lookups return the record projection; add
?expand=payload for the full normalized document (can be large —
up to ~1 MB for history-rich records).
| field | meaning |
|---|---|
| jurisdiction_code, uid | the stable identity — always present |
| office | registering office per the source record; differs from jurisdiction_code for international (e.g. Madrid 'WO') records |
| application_number, registration_number | office numbers; null when the office has not assigned one (~13% of records lack a registration number — pending and abandoned marks legitimately have none) |
| mark_name | the mark text as filed |
| status | normalized: Registered · Pending · Abandoned · Cancelled · Unknown |
| kind_of_mark, mark_feature | kind (Individual, Collective, Certificate, …) and mark type (Word, Figurative, Combined, …) as normalized enums |
| tm5_status | the office's TM5 status string where provided |
| application_date, registration_date, publication_date, expiry_date, renewal_date, termination_date, opposition_period_end | office dates; null means the office did not state it (~36% of records have no expiry date). Null is data — we never substitute defaults. |
| status_date | when the office last changed the status — the office-side change signal |
| external_update_date | office-side record timestamp, where the office publishes one |
| record_update_date | Pinakel pipeline batch date (attribute only — sync on /changes, not this) |
| nice_classes, vienna_codes | Nice classification numbers; Vienna figurative-element codes |
| owner_names, owner_countries, representative_names | normalized party data — names lowercased (original script AND Latin translation where the office provides one), country codes uppercased. owner_names merges applicant/owner/holder; representative_names merges representative/agent/correspondent. Original names, roles and full addresses live in the payload |
| international_registration_numbers, priority_numbers, designated_countries | family/Madrid linkage keys (uppercased); null where the record carries none |
| first_seen_at | when Pinakel first ingested this record |
| updated_at | when the record content last changed in Pinakel — the /changes sync key |
| payload | full normalized document (only with expand=payload; the key is absent otherwise, never null) |
Errors & limits
Every non-2xx response carries one envelope:
{"error": {"code": "…", "message": "…", "param": "…"}} —
code is the machine-readable contract, param names
the offending parameter when one exists.
| status | code | meaning |
|---|---|---|
| 400 | invalid_parameter · unknown_parameter · missing_filter · reserved_parameter · query_too_broad · cursor_invalid · cursor_query_mismatch | malformed request — the message says exactly what to fix |
| 401 | unauthorized | missing or unknown API key |
| 403 | key_revoked | revoked key |
| 404 | not_found | no such record / jurisdiction — a real answer, not an error to retry |
| 422 | query_timeout | query too broad to answer in 10 s — narrow it (do not blind-retry) |
| 429 | rate_limited · quota_exceeded | per-minute rate or monthly quota — see headers |
| 503 | unavailable | transient service problem — safe to retry shortly |
A rate-limit 429 carries Retry-After and
X-RateLimit-Limit / X-RateLimit-Remaining; a quota 429 carries
X-Quota-Limit / X-Quota-Used (resets at month end). Requests
refused with 429 are never counted against your usage.
Every served call costs exactly one metered unit, regardless of page size.
Coverage
Live from the serving database — 2 jurisdictions, 76,067 records ready right now. Coverage grows as backfills complete; this table is generated per-request and never overstates.
| jurisdiction | records | daily frontier |
|---|---|---|
| AD | 49,266 | 2026-09-18 |
| AL | 26,801 | 2026-09-18 |
Support
Integration questions: support@pinakelai.com · commercial: hello@pinakelai.com