Reports & oppositions
This guide covers the registrability- and opposition-focused reads of the Tarno contract: a
brand's registrability report, a brand's opposition timeline, and the listing of open
oppositions in a Niza class ranked by similarity. All three endpoints use the brands:read
scope and have an MCP equivalent.
The full, always-current per-field schema lives in the interactive OpenAPI reference at
/docs(Scalar). This guide explains the reports-and-oppositions contract and links to that reference — it does not restate the schema. When a field is not documented here,/docsis authoritative.
Throughout this guide the base URL is written as $TARNO_BASE_URL and the key as $TARNO_API_KEY,
and every call carries the X-API-Key header — just like the
getting-started guide and the authentication guide.
What this guide covers
Three reads, all under /v1 and all scoped brands:read:
- Registrability report (
GET /v1/brands/:nroSolicitud/report) — a brand's detail as a superset, with extra fields useful for assessing registrability. - Opposition timeline (
GET /v1/brands/:nroSolicitud/opposition) — the chronological sequence of a brand's opposition events, plus the derived opposition window (when applicable). - Open oppositions (
GET /v1/oppositions/open) — brands with an open opposition window in a Niza class, ranked by similarity to a portfolio.
These routes are read-only and honest by design: they never fabricate dates or deadlines the corpus cannot support. When a datum does not exist, the field is omitted (see the honesty notes in each section).
Registrability report
GET /v1/brands/:nroSolicitud/report — scope brands:read. MCP: get_brand_report.
Returns a BrandReport, which is a superset of the brand detail: it includes everything
GET /v1/brands/:nroSolicitud returns (classes, persons, Estado-Diario annotations) plus the
extra fields useful for assessing registrability. An unknown application number returns
404 not_found.
Extra fields over the base detail:
| Field | Type | Notes |
|---|---|---|
nroRegistro |
string | omitted | Registration number. Omitted when the brand has no registration — it never arrives as null. |
estado |
string | The brand's current status. |
tipoSigno |
string | Sign type (denominative, mixed, figurative, …). |
fechaPresentacion |
YYYY-MM-DD |
Filing date. |
fechaVencimiento |
YYYY-MM-DD | null |
Registration expiry date. |
fechaPublicacion |
YYYY-MM-DD | null |
Estado-Diario publication date. |
fechaRegistro |
YYYY-MM-DD | null |
Registration date. |
tipoNombre |
string | Brand-type name. |
subtipoNombre |
string | null | Subtype name, if applicable. |
traduccion |
string | null | Declared translation of the sign, if applicable. |
descripcionEtiqueta |
string | null | Label description. |
protectionDescription |
string | null | Description of the protection scope. |
imagenUrl |
string | null | URL of the sign's image (mixed/figurative brands). |
renovadaDe |
string | null | Application number of the brand this one renews. |
renovadaPor |
string | null | Application number of the brand that renews this one. |
titulares[] |
array\<Persona> | Holders, separated by role. Each person carries its classification, comuna and region (see below). |
representantes[] |
array\<Persona> | Representatives, separated by role. |
completeness |
object | Honesty block. ALWAYS present (see note). |
coverageThrough |
object | omitted | Coverage frontier. Omitted if there is no enriched instance (see note). |
estadoDerivado |
string | omitted | The status the corpus can actually justify, from the same controlled catalogue as estado. Omitted when there is nothing to derive. |
estadoDesactualizado |
boolean | ALWAYS present. True when the corpus already holds the proof of the grant that estado does not reflect yet. |
estadodoes not change: it is still INAPI's RAW value.estadoDerivadoandestadoDesactualizadoare added fields, never a rewrite ofestado. They exist because INAPI assigns the registration number and date before it flips the status: measured, it flips on day ~4-6 after the registration date and only reaches 100 % on day 9, while the Estado-Diario grant annotation lands the same day it is declared. Inside that window the record read "in progress, no registration" over a file that was already titled.estadoDerivadopublishes what the corpus evidence proves, andestadoDesactualizadoflags that the two disagree.
nroRegistrois also omitted when INAPI sends its zero sentinel. INAPI publishes "no registration number" as a zero (numeric0in the Buscador detail, the text"0"in the open-data CSV). A zero is not a registration number, so the report treats it exactly like absence: it omits the field. It previously emittednroRegistro: "0", which broke the contract's own omission promise and could appear next toestadoDerivado: "registrada"in the same payload. A consumer that already checked whether the field was present needs no change: it simply stops receiving the zero. The file identifier isnroSolicitud, a different field that is always present and is untouched by this.
titulares[] and representantes[] are separated by role. A person whose role is 'ambas'
(holder and representative) appears in both arrays.
Two honesty blocks.
completenessis always present and declares how complete the returned record is.coverageThroughis the data's coverage frontier and is omitted when the brand has no enriched instance — its absence means "no known frontier", never a made-up value.
Current holding, and history
personas[], titulares[] and representantes[] carry only who holds it today. A mark that
changed hands no longer names its former owner among the current ones — it used to, and that was two
equally-current holders with one of them false.
The former ones live in personasHistoricas[], with two extra fields:
| field | what it is |
|---|---|
retiradoEn |
the date INAPI says ownership moved, not when we processed it |
retiradoPor |
the annotation that proves it: A07T full transfer, A07P partial, MSO08 assignment, A01 name change |
Always present, almost always empty. A non-empty one is itself the signal that a mark changed hands.
Co-ownership is not history. Two simultaneous holders are legitimate and both stay in
personas[]: someone is retired only when an annotation names the acquirer.
Classes that cover the mark, and those that no longer do
A mark is registered in classes, and they do not live or die together. clases[] now carries
only the ones covering it today; the rest move to clasesHistoricas[], each with INAPI's own
reason in estado.
Measured: 10.232 marks carry classes in mixed states, and 8.220 of those are registrada —
the mark is alive while some of its classes were rejected or never renewed. FARMACIAS AHUMADA
(application 1113943) is registered with 34 classes: 7 cover it, 27 were not renewed. All 34
used to be listed alike.
| covers today | no longer covers |
|---|---|
(C) Concedida · (X) En Trámite · (1) Para conceder · (2) Para rechazar |
(N) Rechazada · (A) Abandonada · (D) Desistida · (V) Vencida · (3) No renovada · (O) Cancelada voluntariamente · (U) Anulada · (I) Dividida · (T) Transferida · (P) Transferida Parcialmente |
(2) Para rechazar still covers. It is a proposed decision, not a final one; calling it fallen
would publish an outcome that has not happened.
(T), (P) and (I) did not die — they MOVED. The class was transferred or divided and still
protects someone, just not under this mark, which is what this answers.
No estado ⇒ it stays in clases[]. Not knowing is not evidence that something ended. The raw
estado travels exactly as INAPI publishes it, so you can judge for yourself.
Charges over the mark
gravamenes[] answers what charges are still there, not whether there ever was one. That much was
already in anotaciones[], but reading it meant knowing that A05I registers a pledge and A05A
lifts it. Here each registration already comes paired with the release that closed it.
| field | what it is |
|---|---|
tipo |
prenda, precautoria, embargo, prohibicion or otros |
estado |
activo (registered, not lifted) or alzado |
inscritoEn / alzadoEn |
INAPI's two dates |
aFavorDe |
the creditor, taken from the registration only |
seccionInscripcion / seccionAlzamiento |
the codes that prove it |
alzamientoObservable |
whether INAPI publishes any code able to lift this kind of charge |
Active ones come first, most recent within that.
aFavorDe is the creditor, never the holder. The phrase "A Favor de:" changes meaning between the
two sides: on the registration it names whoever receives the security, on the release whoever gets the
mark back free. That is why it is only read from the registration side. Measured: on pledges, the
release's "A Favor de:" is one of the mark's own holders 81,6 % of the time, the registration's 0,7 %.
⚠ alzamientoObservable: false means that activo cannot be asserted. For prohibición and otros,
INAPI publishes no code that lifts them: they read as un-lifted because we have no way of finding
out, not because they are still in force. Do not use them to claim a mark is encumbered today.
A release can arrive without its registration (inscritoEn: null): the charge was lifted and its
registration predates the history we hold. It counts as alzado, never as active.
Always present, almost always empty: across the whole corpus, 2.790 marks carry any registered charge and 2.048 an active one.
A licence is not a charge. A03I/A03A form an identically-shaped pair and are deliberately
excluded: it is a right granted over the mark, not an encumbrance against it.
Territorial scope of persons
Every person in personas[], titulares[] and representantes[] carries two more fields:
| Field | Type | Notes |
|---|---|---|
comuna |
string | null | CUT comuna code, no leading zero: '13114' (Las Condes), '5109' (Viña del Mar) |
region |
string | null | CUT region code, no leading zero: '13', '5' |
comunaNombre |
string | null | That comuna's name ('Las Condes'), resolved from the catalog |
regionNombre |
string | null | That region's name ('Región Metropolitana') |
Since 2026-08-25 the codes travel with their name. They are still not the street address —
that is not exposed. Names are the ones INAPI publishes, with its own spellings (Valparaiso
without the accent); null when the code is not in the catalog, never invented.
Coverage: 100 % of regions and 99.82 % of comunas.
- The province comes out of the code itself. Left-pad the comuna to 5 digits and take the first
3:
05109→051(Valparaíso province),03301→033(Huasco). No lookup table is needed to scope by province. 99/99999/999is the unknown-or-foreign sentinel, not a real region.- They always travel together: either both null, or both set.
- Measured coverage (2026-08-24): 70.0 % of holders and 96.4 % of representatives.
What KIND of party each person is
Since 2026-09-08 every person in personas[], titulares[] and representantes[] carries five
more fields, identical to the ones the brand detail and people search return.
They are additive: no earlier field was removed, renamed or re-typed, and no scope changed — they
are read under the same brands:read — so an existing consumer keeps parsing the report unchanged.
| Field | Type | Notes |
|---|---|---|
tipo |
natural | juridica | desconocido |
What kind of party this is. desconocido is a value, not a hole: a third of the corpus carries neither a RUT nor a company suffix, and the contract says so instead of deducing it. |
tipoOrigen |
rut | sufijo | manual | omitted |
Which signal produced tipo. With no signal the key disappears — it never arrives as null. |
observacion |
string | null | The jurisdiction clause INAPI writes glued to the name («…, SOCIEDAD ORGANIZADA BAJO LAS LEYES DE…»), lifted out of nombre. It is INAPI's prose, with 681 distinct spellings of the same clause: not a controlled vocabulary. |
nombrePublicado |
string | null | The spelling exactly as INAPI published it. |
revision |
boolean | The corpus flagged the row for a human to look at. The reason is not published. |
- A
tipo = 'natural'filter is not "everything that is not a company". It would sweep in some 130,000 parties nobody classified. When you need certainty, requiretipoOrigen: 'rut'. nombreis the field to SEARCH with;nombrePublicadois the one to SHOW in the report, or to CHECK against the INAPI document.nombreis the canonical form (UPPERCASE with accents); matching onnombrePublicadore-introduces the case-and-accent noise the canonical form removes.- The people corpus was rewritten on 2026-09-08: 396,444 names moved to the canonical form (~240,000 changed spelling) and 179 duplicate groups were fused without any mark changing hands. Comparing a report cached before that date against one from today will show case and accent differences that are not data changes.
Identity of each annotation
Every element of anotaciones[] carries an id (opaque string). It is stable: corpus writes
are append-only, so an annotation keeps its id for as long as it exists.
!!! warning "What the id does NOT fix"
The key that decides whether an annotation gets written at all is
(nro_solicitud, tipo, fecha, md5(observacion)) and it does not include the section. For a
code outside M1..M14 with no observation, that allows at most one annotation per mark per
day: a second section on the same day is never written and therefore has no id.
Measured: 5.37 % of what the source offers. See the schema.
!!! danger "historyIncomplete: false does not guarantee no annotation is missing"
It is a narrow negative: it means "no Sheets diff pending Buscador confirmation". It is
not raised by a movement that does not change estado, nor by the Buscador lagging the
Estado Diario, nor by the key collision above. coverageTier (denso/residual) is the
corpus era, not a claim about the mark. To bound what you can actually assert, use
dataAsOf, coverageThrough and enrichmentPending.
curl
curl -sS "$TARNO_BASE_URL/v1/brands/123456/report" \
-H "X-API-Key: $TARNO_API_KEY"
TypeScript (fetch)
const res = await fetch(`${process.env.TARNO_BASE_URL}/v1/brands/123456/report`, {
headers: { "X-API-Key": process.env.TARNO_API_KEY! },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message} (requestId=${error.requestId})`);
}
const report = await res.json(); // BrandReport — superset of the brand detail
console.log(report.estado, report.completeness);
// coverageThrough may be omitted if there is no enriched instance.
Brand opposition timeline
GET /v1/brands/:nroSolicitud/opposition — scope brands:read. MCP: get_brand_opposition.
Returns an OppositionTimeline: a flat, chronological list (fecha ascending) of the
Estado-Diario annotations whose section type is 'oposicion', plus the derived opposition window
when the brand has a publication date. An unknown application number returns 404 not_found.
| Field | Type | Notes |
|---|---|---|
nroSolicitud |
string | The brand's application number. |
estado |
string | omitted | Raw status. Omitted if NULL. |
eventos[] |
array\<Evento> | Section-'oposicion' annotations, in chronological order (fecha ASC). |
ventanaOposicion |
object | omitted | The only derived date. Present only if there is a publication date (see note). |
freshnessNote |
string | ALWAYS present. Honestly declares the Estado-Diario freshness gap. |
dataAsOf |
string | omitted | Data timestamp, if applicable. |
coverageThrough |
object | omitted | Coverage frontier. |
Each eventos[] element:
| Subfield | Type | Notes |
|---|---|---|
fecha |
YYYY-MM-DD |
Annotation date. |
seccion |
string | Estado-Diario section code. |
seccionNombre |
string | Human-readable section name. |
seccionTipo |
'oposicion' |
Section type — always 'oposicion' in this timeline. |
observacion |
string | Annotation text. |
And ventanaOposicion (when present):
| Subfield | Type | Notes |
|---|---|---|
fechaPublicacion |
YYYY-MM-DD |
Publication date the window is counted from. |
fechaLimite |
YYYY-MM-DD |
Deadline = publication + 30 business days. |
diasHabilesRestantes |
integer | Business days remaining until the deadline. |
vencida |
bool | true if the window has already closed. |
Honesty — deadlines. The
eventoscarry no per-event deadline: they run from notification dates the corpus does not capture precisely, so no per-event deadline is computed. The only safe derived date isventanaOposicion= publication date + 30 business days, and it is present only if the brand has a publication date — it is never fabricated. In addition,freshnessNoteis always present and declares the Estado-Diario freshness gap, so you know how far the timeline may lag reality.
curl
curl -sS "$TARNO_BASE_URL/v1/brands/123456/opposition" \
-H "X-API-Key: $TARNO_API_KEY"
TypeScript (fetch)
const res = await fetch(`${process.env.TARNO_BASE_URL}/v1/brands/123456/opposition`, {
headers: { "X-API-Key": process.env.TARNO_API_KEY! },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message} (requestId=${error.requestId})`);
}
const timeline = await res.json(); // OppositionTimeline
console.log(timeline.freshnessNote); // always present
for (const ev of timeline.eventos) {
console.log(ev.fecha, ev.seccionNombre, ev.observacion);
}
// ventanaOposicion exists only if the brand has a fechaPublicacion.
if (timeline.ventanaOposicion) {
console.log("Deadline:", timeline.ventanaOposicion.fechaLimite, "closed:", timeline.ventanaOposicion.vencida);
}
Open oppositions in a class
GET /v1/oppositions/open?clase=X&(q=…|nroSolicitud=…) — scope brands:read. MCP: get_open_oppositions.
Lists brands with an open opposition window in a Niza class, ranked by similarity to a
portfolio. The use case: watch registration threats in your class — freshly published brands whose
opposition window is still open and that resemble something you want to protect. The ranking uses the
same similarity model as search / find-similar (phonetic + lexical + semantic).
Query:
| Param | Type | Notes |
|---|---|---|
clase |
integer | REQUIRED. Niza class, integer 1–45. |
q |
string | Free text. Exactly one of q or nroSolicitud (see note). |
nroSolicitud |
string | An existing brand as reference. Exactly one of q or nroSolicitud (see note). |
The
q/nroSolicitudxor. You must sendclaseand exactly one ofq(free text) ornroSolicitud(an existing brand acting as the similarity reference). Sending both or neither returns400 validation_error.
Response: BrandSummaryPage = { items: BrandSummary[], nextCursor: string | null, dataAsOf? }.
Pagination is keyset-based: for the next page, resend nextCursor unchanged as the cursor;
nextCursor: null = end — same keyset semantics as getting started.
Each items element is the same BrandSummary shape documented in
getting started and in /docs.
curl
curl -sS "$TARNO_BASE_URL/v1/oppositions/open?clase=43&q=cafe%20example" \
-H "X-API-Key: $TARNO_API_KEY"
TypeScript (fetch) — paginating
const baseUrl = process.env.TARNO_BASE_URL!;
const apiKey = process.env.TARNO_API_KEY!;
let cursor: string | null = null;
do {
const url = new URL(`${baseUrl}/v1/oppositions/open`);
url.searchParams.set("clase", "43");
url.searchParams.set("q", "cafe example"); // exactly one of q | nroSolicitud
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { "X-API-Key": apiKey } });
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message} (requestId=${error.requestId})`);
}
const page = await res.json(); // BrandSummaryPage
for (const brand of page.items) console.log(brand.nroSolicitud, brand.denominacion, brand.score);
cursor = page.nextCursor; // null = end
} while (cursor);
Scopes
All three endpoints require the brands:read scope.
An empty scopes array is unrestricted (same rule as the rest of the contract): a key with no
scopes is granted all read scopes, including brands:read. A key with a non-empty scopes array is
granted only those listed; calling one of these routes without brands:read is rejected with
403 forbidden naming the missing scope.
See the authentication guide for key lifecycle and scope semantics.
MCP
REST is equivalent to MCP: same key, same scopes, same Zod schemas. The MCP server exposes these tools:
| MCP tool | REST equivalent |
|---|---|
get_brand_report |
GET /v1/brands/:nroSolicitud/report |
get_brand_opposition |
GET /v1/brands/:nroSolicitud/opposition |
get_open_oppositions |
GET /v1/oppositions/open |
See the MCP guide for the endpoint, transport, and authentication.
Errors
Every error uses the uniform envelope { error: { code, message, requestId } } — described in
getting started.
| HTTP | code |
When |
|---|---|---|
| 401 | unauthorized |
Missing, malformed, unknown, or revoked key. |
| 403 | forbidden |
Missing the required brands:read scope. |
| 404 | not_found |
Nonexistent brand (in report and opposition). |
| 400 | validation_error |
In oppositions/open: missing clase, or the q / nroSolicitud xor is not met (both or neither). |
See also
- Authentication and scopes —
X-API-Key,brands:read, key lifecycle. - Connect your AI agent (MCP) — the same reports-and-oppositions tools over MCP.
- Getting started — base URL, keyset pagination, error envelope,
BrandSummary. - The live OpenAPI reference at
/docs— the authoritative per-field schema.