Skip to content

Field-scoped search

This guide documents the field-scoped search surface of GET /v1/brands (and its MCP twin search_brands): the full parameter list, the AND semantics across fields, the q + fields rule, RUT normalization, the new matchedFields[] field, and client-selectable ordering with sort=. All of these parameters are additive and backward-compatible: an existing call behaves identically except for the deliberate change to the no-text default order (see Ordering and sort=).

The complete, always-current per-field schema lives in the interactive OpenAPI reference at /docs (Scalar). This guide explains the combined-search semantics 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. The endpoint uses the brands:read scope.

What this guide covers

GET /v1/brands is a single route that serves two freely combinable modes:

  • Text searchq (or nombre) ranks by the denomination channels (exact / fuzzy / phonetic / semantic, fused with RRF).
  • Field filtering — exact, range and person predicates (clase, estado, nroSolicitud, titular, rut, …) that narrow the result set.

All present parameters combine with AND (see AND semantics). The response is a keyset page — the same { items: BrandSummary[], nextCursor } shape as getting started.

Already have a list of application numbers? To get the summary of several known marks (a portfolio, a watchlist) at once, don't search — use POST /v1/brands/batch, which resolves them all in one call (see Batch lookup).

Parameters

GET /v1/brands accepts these query parameters. All are optional. A multi-value parameter is repeated in the query (?clase=9&clase=25) and does OR within the field; distinct fields do AND across each other.

Param Type Multi-value Alias Notes
q string no Free text (Spanish FTS + fuzzy + phonetic + semantic, RRF). Discarded only when a competitor field arrives (nombre/denominacion/nroSolicitud/nroRegistro) → advanced mode; narrowing filters (clase, estado, tipoSigno, fechaPresentacion*, titular, representante, rut) keep q and just narrow it (see q + fields rule).
nombre string no denominacion Text over the denomination, same engine as q.
denominacion string no nombre Alias of nombre.
nroSolicitud string no Application number, exact match.
nroRegistro string no Registration number, exact match.
titular string yes Substring over the holder's name/legal name (role titular/ambas), accent- and case-insensitive.
representante string yes Substring over the representative's name (role representante/ambas), accent- and case-insensitive.
rut string yes Normalized RUT (see RUT); exact match against holder and representative.
vigencia enum no vigente Scopes titular/representante/rut to current holding (vigente), to what is no longer held (historica), or both (todas). Without one of those three filters → 400.
clase integer yes Nice class, 145.
estado string yes Status code (controlled catalog).
tipoSigno string yes Sign-type code (controlled catalog).
fechaPresentacionDesde YYYY-MM-DD no fechaDesde Lower bound of the filing date (fecha_presentacion >=), inclusive.
fechaPresentacionHasta YYYY-MM-DD no fechaHasta Upper bound of the filing date (fecha_presentacion <=), inclusive.
sort enum no Result ordering (see Ordering and sort=). relevance (=relevancia, default) · fechaPresentacion:desc/:asc (=recientes/antiguas) · fechaActualizacion:desc/:asc · denominacion:asc/:desc · estado/estado:asc/estado:desc.
limit integer no Page size. Default 20, max 100.
cursor string no Opaque keyset cursor (see pagination). Treat it as a black box. Mutually exclusive with offset.
offset integer no Offset pagination (see pagination). 010000. Only with a non-relevance sort; mutually exclusive with cursor.

A sort value outside the enum returns 400 validation_error (the uniform error envelope). A syntactically invalid rut returns 400 invalid_rut (see RUT).

Current holding, and how to ask for the previous one

titular, representante and rut match whoever holds it today by default. A mark that changed hands stops showing up under its former owner — it used to, and that was a wrong answer: it returned a portfolio that person no longer has.

vigencia changes the question:

value what you get
vigente (default) what the person holds now
historica what they used to hold and no longer do
todas both — the previous behaviour, now explicit
curl -sS "$TARNO_BASE_URL/v1/brands?titular=Kangol&vigencia=historica" -H "X-API-Key: $TARNO_API_KEY"

It scopes only those three filters, so without one of them the response is 400: it would filter nothing, and a caller who asked for historica and quietly got the default would read current marks as a person's history.

Who held a specific mark before lives in personasHistoricas on the detail — see reports.

AND semantics

All present parameters combine with AND. Each field narrows the result further:

# Marks in class 9 OR 25 (OR within clase), AND whose holder contains "sonda",
# AND filed from 2020 onward (AND across fields).
curl -sS "$TARNO_BASE_URL/v1/brands?clase=9&clase=25&titular=sonda&fechaPresentacionDesde=2020-01-01" \
  -H "X-API-Key: $TARNO_API_KEY"
  • OR within a multi-value field: ?clase=9&clase=25 = class 9 or 25.
  • AND across distinct fields: adding titular= narrows to those that also satisfy that predicate.

There is no OR between fields and no parenthesized DSL in v1 — field-wise AND covers the motivating case (IP lawyers narrowing by holder/class/date). The empty set of field-scoped parameters does not change behavior relative to the base contract.

The q + fields rule (advanced mode)

q is discarded only when it arrives together with a competitor text/identity field — nombre/denominacion (alias), nroSolicitud or nroRegistro: then advanced mode kicks in and the field-combined search takes over. Narrowing filters (clase, estado, tipoSigno, fechaPresentacion*, titular, representante, rut) do not discard q: they keep its text search and just narrow it with AND. This is the authoritative reading of the contract (inherited from the combined-search release):

  • ?q=cafe alone ⇒ classic free-text search (ranks by relevance, carries score).
  • ?q=cafe&clase=43narrowing (not advanced mode): q is kept; the result is "Café marks in class 43" — the text ranks and clase narrows it — ordered by relevance unless an explicit sort= is given (see Ordering).
  • ?q=cafe&nombre=nescafeadvanced mode: q is discarded (a competitor field arrived); the nombre= text drives the field-combined search, ordered by the effective sort mode (see Ordering).
  • To search by denomination inside advanced mode, use nombre= (not q=): nombre= is a competitor field and does participate in the AND.

In other words: q is the single-search-box simple mode; the moment you combine text with a competitor field (nombre/nroSolicitud/nroRegistro), switch to nombre for text. Narrowing by clase/estado/dates/persons needs no change: q stays active and is simply narrowed.

Ordering and sort=

The sort= parameter selects result ordering. Explicit sort= always wins over the automatic order:

sort Order Pagination Notes
relevance (=relevancia, default) By relevance (RRF) when there is text; otherwise fecha_presentacion DESC keyset With text it ranks by the denomination channels; with no text it silently falls back to date DESC (not an error).
fechaPresentacion:desc (=recientes) fecha_presentacion DESC, nro_solicitud ASC keyset or offset Wins even with text: text still filters, but the final order is by date.
fechaPresentacion:asc (=antiguas) fecha_presentacion ASC, nro_solicitud ASC keyset or offset Deterministic mirror.
fechaActualizacion:desc / :asc By the record's last update (updated_at) offset updated_at is refreshed on every re-scrape/upsert of the mark.
denominacion:asc / :desc Alphabetical by mark text (accent/case-insensitive) offset Accent/case-folded order (Ñ, accents normalized).
estado / estado:asc / estado:desc Grouped by the trámite lifecycle offset Not alphabetical. Order: en trámite → observación de fondo → publicada → oposición → concedida → registrada → esperando renovación → rechazada → denegada → desistida → abandonada → anulada → caducado → vencida; unknown/null state last.

An out-of-list sort returns 400 validation_error. The aliases relevancia/recientes/antiguas remain valid (equal to relevance/fechaPresentacion:desc/fechaPresentacion:asc).

Default order (no sort=):

  • With text (q or nombre) ⇒ relevance (RRF).
  • Without textfecha_presentacion DESC, nro_solicitud ASC (newest first).

Behavior change. The no-text listing (?clase=, ?estado= alone) changed its default order from nro_solicitud ASC to fecha_presentacion DESC, nro_solicitud ASC (newest first). If you need the old order by application number, there is no sort value for it today; the available stable order is by date. In-flight keyset cursors from no-text listings reset once after the change (an old cursor= from a no-text listing returns to page 1; just restart pagination). See the changelog for the release note.

Marks with a null fecha_presentacion come last in both modes (recientes and antiguas).

Reach ceiling with text (F-4). With text (q/nombre/denominacion), recientes/antiguas re-order the relevance-capped fused candidate set (≈600 marks: the top candidates per channel) by date — not the full matching corpus. So ?q=<common term>&sort=recientes returns the newest among the top-relevance matches, not the newest of all matches. The no-text listing (sort= without q) has no such ceiling: it pages the full corpus by date over the date index. For full-corpus date paging, list without text (or narrow by clase/estado/fechaPresentacion*).

matchedFields[] — why this mark surfaced

Each items[] element may include matchedFields[], an additive array that answers "why did this mark surface?". It is optional and omitted when there is no identity/text match (e.g. a pure ?clase=9 listing, which only narrows).

Each entry is { field, kind }:

  • field — the identity/search field that matched. Closed enum: nombre, nroSolicitud, nroRegistro, titular, representante, rut. The narrowing filters (clase, estado, tipoSigno, fechaPresentacion*) never appear — they are narrowing criteria, not match signals.
  • kind — the channel it matched through. Enum: exact, fuzzy, phonetic, semantic.
  • nroSolicitud, nroRegistro, rut ⇒ always exact.
  • titular, representantefuzzy (accent/case-insensitive substring).
  • nombre ⇒ the dominant channel when it matched several at once, per the hierarchy exact > fuzzy > phonetic > semantic. One entry per field (never one per channel).
{
  "nroSolicitud": "123456",
  "denominacion": "SONDA",
  "score": 0.91,
  "matchedFields": [
    { "field": "nombre", "kind": "exact" },
    { "field": "titular", "kind": "fuzzy" }
  ]
}

matchedFields[] is purely additive: the existing items[] shape does not change in any other respect. A client that does not read it keeps working unchanged.

RUT: normalization, invalid_rut and coverage

The rut parameter matches exactly against persona.identificador of holder and representative (AND with the rest of the predicates). The server normalizes the value before comparing, so we accept the three usual formats:

Input Normalizes to
76.123.456-7 76123456-7
761234567 76123456-7
76123456-7 76123456-7

The check digit (DV) is validated with modulo 11; the DV may be 09 or K (uppercase).

invalid_rut error. A syntactically invalid rut (whose DV does not compute) returns 400 with { "error": { "code": "invalid_rut" } } — the uniform error envelope. Never 500 nor a silent empty result set. This validation and error code are identical across REST and MCP.

Partial RUT coverage. Not every mark in the corpus has a RUT attached to its persona. As a rough guide, around ~43% of marks carry a RUT on the holder and ~71% carry one on the representative. A valid rut that is not in the corpus returns an empty result — this is correct (partial coverage), not an error. Combine rut with titular/nombre when you want to cover marks with no registered RUT.

Pagination: keyset and offset

There are two pagination modes. Pick one; mixing them (cursor and offset in the same request) returns 400 validation_error.

Keyset (by cursor) — the default

Stable and gap-free, for sort=relevance (or no sort) and the date orders (fechaPresentacion:* / recientes / antiguas). Each response carries a nextCursor:

  • For the next page, resend nextCursor unchanged as the cursor parameter.
  • nextCursor: null means end of results — stop.
  • The cursor is opaque (base64url of the order keys). Treat it as a black box; its internal shape depends on the effective sort mode and may change without notice.

sort=relevance (or no sort) paginates by keyset only: it does not accept offset.

Offset (by displacement) — to jump to page N

For the fechaActualizacion:*, denominacion:* and estado[:*] orders — and, if you prefer, the date orders too — use offset + limit:

  • offset is the number of results to skip (010000); limit is the page size.
  • In offset mode nextCursor is null: to advance, increment offset (e.g. page 3 with limit=20offset=40). Use estimatedTotal (present in every mode) to know how many pages.
  • The offset cap is 10000 (it protects the database): a larger offset returns 400 validation_error. To reach further, narrow with filters (clase/estado/dates/text).

Rules (anything invalid is 400 validation_error, never silently ignored):

Combination Result
cursor and offset together 400 validation_error
offset with sort=relevance (or no sort and text present) 400 validation_error (relevance is keyset-only)
cursor with fechaActualizacion/denominacion/estado 400 validation_error (those orders are offset-only)
offset > 10000 400 validation_error

Compatibility: existing calls (no sort or relevancia/recientes/antiguas, with cursor) keep working exactly the same — offset mode is purely additive.

See getting started for the full pagination pattern.

Batch lookup

When you already have a list of application numbers — a portfolio, a watchlist — and want the summary of all of them at once, use POST /v1/brands/batch instead of N calls to GET /v1/brands/{nroSolicitud}. It resolves up to 100 application numbers in one call that costs a single quota unit (not one per mark). Same brands:read scope.

Body (application/json): { "nros": [ … ] }.

curl -sS -X POST "$TARNO_BASE_URL/v1/brands/batch" \
  -H "X-API-Key: $TARNO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"nros":["1286842","823470","9999999"]}'

Response (200):

{
  "items": [
    { "nroSolicitud": "1286842", "denominacion": "…", "estado": "…", "clases": [9, 42], "titular": ["…"], "enrichmentPending": false },
    { "nroSolicitud": "823470",  "denominacion": "…", "estado": "…", "clases": [30],     "titular": [],    "enrichmentPending": false }
  ],
  "notFound": ["9999999"],
  "dataAsOf": "2026-07-20T03:17:00.000Z"
}
  • items — the BrandSummary rows that were found, in the same order you requested the nros. It is the same enriched summary (clases, titular, enrichmentPending) that the GET /v1/brands listing returns. Because there is no search text, they carry no score, matchedBy or matchedFields.
  • notFound — the requested nros that do not exist in the corpus (in request order). Not an error: you asked for a non-existent mark and we tell you so explicitly.
  • dataAsOf — the same optional freshness stamp as the rest of the contract.

Rules:

  • Repeated nros collapse to one (a number appears once, in items or in notFound).
  • Returns the summary, not the detail. For a single mark's full detail (classes + persons + annotations) use GET /v1/brands/{nroSolicitud}; batch is for reviewing many marks at once, not one mark's file.
  • Cap of 100. A batch with more than 100 nros returns 422 invalid_input. A body with an empty or malformed nros returns 400 validation_error.
  • One quota unit per call, regardless of how many nros you request — that is the whole point: it avoids the N+1 (and the N quota units) of N individual calls.

MCP parity

REST is equivalent to MCP: same key, same scopes, same Zod schemas, same query layer. The MCP tool search_brands accepts exactly the same field-scoped parameters documented here — including sort, rut normalization and the invalid_rut error — with identical semantics. The response (including matchedFields[] and ordering) is byte-identical to REST, verified with a deep-equal parity test against real Postgres. Batch lookup has its own twin, get_brands_batch, with the same { items, notFound, dataAsOf } shape and the same cap of 100 (invalid_input).

MCP tool REST equivalent
search_brands GET /v1/brands
get_brands_batch POST /v1/brands/batch

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
400 validation_error Invalid parameters (e.g. sort outside the enum, clase outside 145, malformed date, batch body with an empty nros).
400 invalid_rut A rut whose check digit does not compute (modulo 11).
422 invalid_input A batch (POST /v1/brands/batch) with more than 100 nros (see Batch lookup).
401 unauthorized Missing, malformed, unknown or revoked API key.
403 forbidden Missing the required brands:read scope.
429 rate_limited / quota_exceeded Short-term burst limit or monthly quota exceeded.

See also