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,/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. 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 search —
q(ornombre) 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, 1–45. |
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). 0–10000. 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=cafealone ⇒ classic free-text search (ranks by relevance, carriesscore).?q=cafe&clase=43⇒ narrowing (not advanced mode):qis kept; the result is "Café marks in class 43" — the text ranks andclasenarrows it — ordered by relevance unless an explicitsort=is given (see Ordering).?q=cafe&nombre=nescafe⇒ advanced mode:qis discarded (a competitor field arrived); thenombre=text drives the field-combined search, ordered by the effective sort mode (see Ordering).- To search by denomination inside advanced mode, use
nombre=(notq=):nombre=is a competitor field and does participate in theAND.
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 (
qornombre) ⇒ relevance (RRF). - Without text ⇒
fecha_presentacion DESC, nro_solicitud ASC(newest first).
Behavior change. The no-text listing (
?clase=,?estado=alone) changed its default order fromnro_solicitud ASCtofecha_presentacion DESC, nro_solicitud ASC(newest first). If you need the old order by application number, there is nosortvalue for it today; the available stable order is by date. In-flight keyset cursors from no-text listings reset once after the change (an oldcursor=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/antiguasre-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=recientesreturns the newest among the top-relevance matches, not the newest of all matches. The no-text listing (sort=withoutq) 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 byclase/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⇒ alwaysexact.titular,representante⇒fuzzy(accent/case-insensitive substring).nombre⇒ the dominant channel when it matched several at once, per the hierarchyexact > 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 0–9 or K (uppercase).
invalid_ruterror. A syntactically invalidrut(whose DV does not compute) returns400with{ "error": { "code": "invalid_rut" } }— the uniform error envelope. Never500nor 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
rutthat is not in the corpus returns an empty result — this is correct (partial coverage), not an error. Combinerutwithtitular/nombrewhen 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
nextCursorunchanged as thecursorparameter. nextCursor: nullmeans 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:
offsetis the number of results to skip (0–10000);limitis the page size.- In offset mode
nextCursorisnull: to advance, incrementoffset(e.g. page 3 withlimit=20⇒offset=40). UseestimatedTotal(present in every mode) to know how many pages. - The
offsetcap is 10000 (it protects the database): a largeroffsetreturns400 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
sortorrelevancia/recientes/antiguas, withcursor) 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— theBrandSummaryrows that were found, in the same order you requested thenros. It is the same enriched summary (clases,titular,enrichmentPending) that theGET /v1/brandslisting returns. Because there is no search text, they carry noscore,matchedByormatchedFields.notFound— the requestednrosthat 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
nroscollapse to one (a number appears once, initemsor innotFound). - 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
nrosreturns422 invalid_input. A body with an empty or malformednrosreturns400 validation_error. - One quota unit per call, regardless of how many
nrosyou 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 1–45, 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
- Getting started — base URL, first call, keyset pagination, error envelope.
- Authentication & scopes —
X-API-Key,brands:read, key lifecycle. - Connect your AI agent (MCP) — the same search via the
search_brandstool. - Code samples — copy-paste-ready REST and MCP clients.
- The live OpenAPI reference at
/docs— the authoritative per-field schema.