Vigilancia (watchlists)
Vigilancia te deja registrar watchlists que monitorean el corpus de marcas de INAPI y generan hits cuando aparecen coincidencias — presentaciones nuevas, cambios sobre marcas previas o cruces con el histórico (baseline). Cada watchlist entrega sus hits de forma dual: el pull por keyset está siempre disponible, y el push por webhook firmado es opcional.
El esquema completo y siempre actualizado por campo vive en la referencia OpenAPI interactiva en
/docs(Scalar). Esta guía explica el contrato de Vigilancia y enlaza a esa referencia — no reformula el esquema. Cuando un campo no está documentado aquí,/docses autoritativo.
A lo largo de la guía la URL base se escribe como $TARNO_BASE_URL y la clave como $TARNO_API_KEY,
y cada llamada lleva el encabezado X-API-Key — igual que en la
guía de primeros pasos y la guía de autenticación.
Qué es Vigilancia
Una watchlist es un conjunto de ítems a vigilar (marcas por denominación, o números de solicitud concretos) más filtros opcionales. El motor de vigilancia recorre el corpus de forma incremental tras cada sync y, cuando un ítem coincide, registra un hit con un snapshot congelado de la marca detectada.
La entrega es dual:
- Pull (siempre) — lees los hits por keyset con
GET /v1/watch/:id/hits. - Push (opcional) — si registras un
callbackUrl, Tarno te envía un webhook firmado como notificación cada vez que hay hits nuevos. El webhook no transporta hits: te avisa, y tú luego haces el pull.
Scopes y tenencia
Vigilancia usa dos scopes, además de las mismas reglas de scopes que el resto del contrato (ver la guía de autenticación):
| Scope | Otorga acceso a |
|---|---|
watch:read |
Lecturas: GET /v1/watch, GET /v1/watch/:id, GET /v1/watch/:id/hits, GET /v1/watch/:id/items. |
watch:write |
Escrituras: POST /v1/watch, PATCH /v1/watch/:id, DELETE /v1/watch/:id, y los ítems POST/DELETE /v1/watch/:id/items. |
Un arreglo de scopes vacío no tiene restricción (misma regla que brands:read): una clave sin
scopes recibe todos los scopes de lectura, incluidos los de watch. Una clave con scopes no vacíos
recibe solo los listados; llamar a una ruta cuyo scope requerido está ausente se rechaza con
403 forbidden nombrando el scope faltante.
La org dueña se resuelve desde la clave de API (el consumerId de la clave, Phase 8), nunca
desde el cuerpo de la solicitud. Consecuencias:
- Una clave sin org dueña (
consumerIdnulo) no puede crear ni listar watchlists →403con mensaje "API key has no owning org". - Cada clave solo ve sus propias watchlists (las de su org). Una watchlist de otra org es, a
todos los efectos, inexistente →
404uniforme.
En resumen: para usar Vigilancia una clave necesita a la vez una org dueña y los scopes
watch:*. Nota operativa: la clave actual de tarno-app todavía no lleva watch:*.
Crear una watchlist
POST /v1/watch — scope watch:write.
Cuerpo:
| Campo | Tipo | Notas |
|---|---|---|
label |
string | Nombre legible de la watchlist. |
items |
array (≥ 1) | Ítems a vigilar: marcas (por denominación) o números de solicitud (nro). |
callbackUrl |
url | null | Opcional. URL del webhook. null u omitido = solo pull. |
filters |
objeto | Opcional. Ver Filtros. |
baseline |
bool | Opcional. true = cruzar contra todo el corpus histórico una vez (D-14). |
La respuesta 201 devuelve la watchlist creada más un signingSecret (formato whsec_...).
El
signingSecretse devuelve UNA SOLA VEZ, aquí en la creación (y al rotarlo — ver Gestión). Nunca aparece en lecturas (GET). Guárdalo de inmediato en un lugar seguro; si lo pierdes, tendrás que rotarlo.
curl
curl -sS -X POST "$TARNO_BASE_URL/v1/watch" \
-H "X-API-Key: $TARNO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Cafeterías clase 43",
"items": [{ "marca": "CAFE EXAMPLE" }],
"callbackUrl": "https://tu-app.example/webhooks/tarno",
"filters": { "clase": [43], "liveOnly": true }
}'
TypeScript (fetch)
const res = await fetch(`${process.env.TARNO_BASE_URL}/v1/watch`, {
method: "POST",
headers: {
"X-API-Key": process.env.TARNO_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
label: "Cafeterías clase 43",
items: [{ marca: "CAFE EXAMPLE" }],
callbackUrl: "https://tu-app.example/webhooks/tarno",
filters: { clase: [43], liveOnly: true },
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message} (requestId=${error.requestId})`);
}
const watchlist = await res.json();
// ⚠️ signingSecret llega SOLO aquí — guárdalo ahora, nunca vuelve a aparecer.
await storeSecret(watchlist.id, watchlist.signingSecret); // "whsec_..."
Errores de creación:
422 watchlist_too_large— superaste un tope (demasiadas watchlists, o demasiados ítems en una).422 unsafe_callback— elcallbackUrlno pasó la validación anti-SSRF.
Filtros
El objeto filters (jsonb) es opcional y todos sus campos lo son:
| Campo | Tipo | Efecto |
|---|---|---|
minScore |
number (0..1) | Piso de relevancia. Sobrescribe el piso por defecto de la vigilancia (0.7). |
clase |
array\<integer> | Clases de Niza a considerar. |
allClasses |
bool | Ensancha una vigilancia por nro de vuelta a todas las clases (por defecto solo la clase de la marca). |
liveOnly |
bool | Excluye estados terminales (denegada, vencida, anulada). |
Además, baseline: true en la creación habilita el cruce baseline una única vez contra todo el
corpus histórico (D-14).
Gestionar watchlists
Listar — GET /v1/watch
Scope watch:read. Lista solo las tuyas (las de tu org). Nunca incluye el signingSecret.
curl -sS "$TARNO_BASE_URL/v1/watch" -H "X-API-Key: $TARNO_API_KEY"
Obtener una — GET /v1/watch/:id
Scope watch:read. Una watchlist inexistente o de otra org devuelve un 404 uniforme (no se
distingue "no existe" de "no es tuya").
Actualizar — PATCH /v1/watch/:id
Scope watch:write. Cuerpo disperso (sparse): manda solo los campos que cambian.
| Campo | Tipo | Notas |
|---|---|---|
label |
string | Renombra la watchlist. |
callbackUrl |
url | null | Cambia o quita (null) el webhook. |
filters |
objeto | Reemplaza los filtros. |
status |
'active' | 'paused' |
Pausa/reanuda la vigilancia. |
rotateSecret |
bool | true → devuelve un nuevo signingSecret una sola vez en la respuesta. |
Una watchlist desconocida o de otra org → 404.
Retirar — DELETE /v1/watch/:id
Scope watch:write. Es un retiro suave (status = 'retired'): detiene los barridos y el push,
pero preserva los hits, que siguen siendo legibles por pull. Devuelve 204 No Content.
Retirar ≠ pausar. Pausar (
status: 'paused'víaPATCH) es reversible y solo suspende la vigilancia. Retirar esDELETE(no unstatusque puedas enviar), es el fin de vida de la watchlist, y aun así conserva el historial de hits para consulta.
Editar los ítems vigilados
Para ajustar la cartera vigilada sin recrear la watchlist (que generaría un nuevo id y
signingSecret y perdería el historial de hits), edita los ítems de forma incremental:
| Operación | Endpoint | Scope | Respuesta |
|---|---|---|---|
| Listar ítems | GET /v1/watch/:id/items |
watch:read |
200 — array de WatchItem (ver abajo). |
| Agregar un ítem | POST /v1/watch/:id/items |
watch:write |
201 — el WatchItem creado. |
| Quitar un ítem | DELETE /v1/watch/:id/items/:itemId |
watch:write |
204 No Content. |
Un WatchItem es { id: number, watchlistId: string, mark: string, createdAt: string }. El
id numérico es el que pasas a DELETE .../items/:itemId (lo obtienes del listado).
Cuerpo de POST: { "mark": "SONDA" } — una marca (por denominación) o un número de solicitud.
Notas:
- Idempotente: reagregar un
markque ya existe devuelve201con la fila existente y no incrementa el conteo (no hay duplicados). - Tope por tenant: agregar un
marknuevo que superemax_watch_itemsse rechaza con422 watchlist_too_large(igual que en la creación); un reagregado al tope se permite. - Tenencia: una watchlist inexistente o de otra org →
404uniforme; unitemIdque no es de tu watchlist →404.
# Agregar
curl -sS -X POST "$TARNO_BASE_URL/v1/watch/<id>/items" \
-H "X-API-Key: $TARNO_API_KEY" -H "Content-Type: application/json" \
-d '{"mark":"SONDA"}'
# Listar
curl -sS "$TARNO_BASE_URL/v1/watch/<id>/items" -H "X-API-Key: $TARNO_API_KEY"
# Quitar (itemId del listado)
curl -sS -X DELETE "$TARNO_BASE_URL/v1/watch/<id>/items/<itemId>" -H "X-API-Key: $TARNO_API_KEY"
Entrega dual
Pull (siempre) — GET /v1/watch/:id/hits
Scope watch:read. Siempre disponible, haya o no callbackUrl.
| Query | Tipo | Notas |
|---|---|---|
since |
string | Cursor opaco de keyset. Trátalo como caja negra. |
limit |
integer | Tamaño de página. Por defecto 50, máximo 200. |
Respuesta: { items: WatchHit[], nextCursor: string | null }. nextCursor: null = fin — misma
semántica de keyset que primeros pasos. Para la próxima página, reenvía el
cursor sin cambios como since.
curl -sS "$TARNO_BASE_URL/v1/watch/<id>/hits?limit=50" -H "X-API-Key: $TARNO_API_KEY"
Push (opcional) — webhook firmado
Si registraste un callbackUrl, Tarno hace un POST firmado a esa URL cuando hay hits nuevos.
Verifica la firma (abajo) y luego haz el pull de /hits.
Verificación del webhook
El webhook va firmado con HMAC-SHA256. El encabezado es:
X-Tarno-Signature: t=<unix>,v1=<hmac-hex>
El mensaje firmado es `${t}.${rawBody}` (el timestamp t, un punto, y el cuerpo crudo de la
solicitud) usando el signing_secret (whsec_...) de esa watchlist. La tolerancia anti-replay
(skew) es de 300 s.
El webhook NO transporta hits (D-02 / D-11 — nunca lleva cuerpos de resultados): es una notificación. Al recibirlo, verifica la firma, rechaza si el skew excede 300 s, y luego haz el pull de
GET /v1/watch/:id/hits.
Ejemplo de verificación en Node (node:crypto):
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody = el cuerpo CRUDO exacto (Buffer/string), sin re-serializar.
function verifyTarnoWebhook(rawBody: string, header: string, signingSecret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || !v1) return false;
// Rechaza fuera de la ventana anti-replay (300 s).
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", signingSecret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
Advertencia: verifica siempre la firma y rechaza por skew antes de actuar sobre la notificación.
Forma del hit
Cada elemento de GET /v1/watch/:id/hits es un WatchHit:
| Campo | Tipo | Notas |
|---|---|---|
id |
string | Identificador del hit. |
watchItemId |
string | Ítem de la watchlist que coincidió. |
watchlistId |
string | Watchlist a la que pertenece. |
matchedNro |
string | Número de solicitud de la marca detectada. |
triggerHash |
string | Hash de deduplicación del disparo. |
score |
number | Puntaje de relevancia de la coincidencia. |
signals |
objeto | Señales que explican la coincidencia. |
hitKind |
'new_filing' | 'update_on_prior' | 'baseline' |
Tipo de hit. |
snapshot |
BrandSummary | Snapshot congelado de la marca detectada. |
detectedAt |
string (fecha-hora) | Cuándo se detectó. |
Los campos de snapshot son la misma forma BrandSummary documentada en
primeros pasos y en /docs.
MCP
REST es equivalente a MCP: misma clave, mismos scopes, mismos esquemas de Zod. El servidor MCP expone estas herramientas de Vigilancia:
| Herramienta MCP | Equivalente REST |
|---|---|
watch_register |
POST /v1/watch |
watch_list |
GET /v1/watch |
watch_get |
GET /v1/watch/:id |
watch_patch |
PATCH /v1/watch/:id |
watch_delete |
DELETE /v1/watch/:id |
watch_list_hits |
GET /v1/watch/:id/hits |
watch_list_items |
GET /v1/watch/:id/items |
watch_add_item |
POST /v1/watch/:id/items |
watch_remove_item |
DELETE /v1/watch/:id/items/:itemId |
Consulta la guía de MCP para el endpoint, el transporte y la autenticación.
Errores
Todos los errores usan el sobre uniforme { error: { code, message, requestId } } — descrito en
primeros pasos.
| HTTP | code |
Cuándo |
|---|---|---|
| 401 | unauthorized |
Clave ausente, malformada, desconocida o revocada. |
| 403 | forbidden |
Falta el scope watch:* requerido, o la clave no tiene org dueña. |
| 404 | not_found |
Watchlist inexistente o de otra org (uniforme — no distingue ambos casos). |
| 422 | watchlist_too_large |
Superaste un tope (demasiadas watchlists o demasiados ítems). |
| 422 | unsafe_callback |
El callbackUrl no pasó la validación anti-SSRF. |
Ver también
- Autenticación y scopes —
X-API-Key,watch:read/watch:write, ciclo de vida de la clave. - Conecta tu agente de IA (MCP) — mismas herramientas de Vigilancia vía MCP.
- Primeros pasos — URL base, paginación por keyset, sobre de error.
- La referencia OpenAPI en vivo en
/docs— el esquema autoritativo por campo.