Skip to content

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, /docs is 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.

estado does not change: it is still INAPI's RAW value. estadoDerivado and estadoDesactualizado are added fields, never a rewrite of estado. 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. estadoDerivado publishes what the corpus evidence proves, and estadoDesactualizado flags that the two disagree.

nroRegistro is also omitted when INAPI sends its zero sentinel. INAPI publishes "no registration number" as a zero (numeric 0 in 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 emitted nroRegistro: "0", which broke the contract's own omission promise and could appear next to estadoDerivado: "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 is nroSolicitud, 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. completeness is always present and declares how complete the returned record is. coverageThrough is 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: 05109051 (Valparaíso province), 03301033 (Huasco). No lookup table is needed to scope by province.
  • 99 / 99999 / 999 is 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, require tipoOrigen: 'rut'.
  • nombre is the field to SEARCH with; nombrePublicado is the one to SHOW in the report, or to CHECK against the INAPI document. nombre is the canonical form (UPPERCASE with accents); matching on nombrePublicado re-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 eventos carry 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 is ventanaOposicion = publication date + 30 business days, and it is present only if the brand has a publication date — it is never fabricated. In addition, freshnessNote is 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 145.
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 / nroSolicitud xor. You must send clase and exactly one of q (free text) or nroSolicitud (an existing brand acting as the similarity reference). Sending both or neither returns 400 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