Ir para o conteúdo

Vigilância (watchlists)

Vigilância permite registrar watchlists que monitoram o corpus de marcas do INAPI e geram hits quando surgem correspondências — novos depósitos, atualizações sobre marcas anteriores ou cruzamentos com o histórico (baseline). Cada watchlist entrega seus hits de forma dual: o pull por keyset está sempre disponível, e o push por webhook assinado é opcional.

O schema completo e sempre atualizado por campo vive na referência OpenAPI interativa em /docs (Scalar). Este guia explica o contrato de Vigilância e faz o link para essa referência — não reformula o schema. Quando um campo não está documentado aqui, /docs é autoritativo.

Ao longo do guia a URL base é escrita como $TARNO_BASE_URL e a chave como $TARNO_API_KEY, e toda chamada carrega o header X-API-Key — igual ao guia de primeiros passos e ao guia de autenticação.

O que é Vigilância

Uma watchlist é um conjunto de itens a vigiar (marcas por denominação, ou números de solicitação específicos) mais filtros opcionais. O motor de vigilância percorre o corpus de forma incremental após cada sync e, quando um item corresponde, registra um hit com um snapshot congelado da marca detectada.

A entrega é dual:

  • Pull (sempre) — você lê os hits por keyset com GET /v1/watch/:id/hits.
  • Push (opcional) — se você registrar um callbackUrl, o Tarno envia um webhook assinado como notificação sempre que houver novos hits. O webhook não transporta hits: ele avisa, e você então faz o pull.

Escopos e tenancy

Vigilância usa dois escopos, além das mesmas regras de escopos do restante do contrato (veja o guia de autenticação):

Escopo Concede acesso a
watch:read Leituras: GET /v1/watch, GET /v1/watch/:id, GET /v1/watch/:id/hits.
watch:write Escritas: POST /v1/watch, PATCH /v1/watch/:id, DELETE /v1/watch/:id.

Um array de escopos vazio é irrestrito (mesma regra que brands:read): uma chave sem escopos recebe todos os escopos de leitura, incluindo os de watch. Uma chave com um array de escopos não vazio recebe somente os listados; chamar uma rota cujo escopo exigido está ausente é rejeitado com 403 forbidden nomeando o escopo faltante.

A org dona é resolvida a partir da chave de API (o consumerId da chave, Phase 8), nunca a partir do corpo da requisição. Consequências:

  • Uma chave sem org dona (consumerId nulo) não pode criar nem listar watchlists → 403 com a mensagem "API key has no owning org".
  • Cada chave vê somente as suas watchlists (as da sua org). A watchlist de outra org é, para todos os efeitos, inexistente → 404 uniforme.

Em resumo: para usar Vigilância uma chave precisa ao mesmo tempo de uma org dona e dos escopos watch:*. Nota operacional: a chave atual do tarno-app ainda não carrega watch:*.

Criar uma watchlist

POST /v1/watch — escopo watch:write.

Corpo:

Campo Tipo Notas
label string Nome legível da watchlist.
items array (≥ 1) Itens a vigiar: marcas (por denominação) ou números de solicitação (nro).
callbackUrl url | null Opcional. URL do webhook. null ou omitido = somente pull.
filters objeto Opcional. Veja Filtros.
baseline bool Opcional. true = cruzar contra todo o corpus histórico uma vez (D-14).

A resposta 201 retorna a watchlist criada mais um signingSecret (formato whsec_...).

O signingSecret é retornado UMA ÚNICA VEZ, aqui na criação (e ao rotacioná-lo — veja Gerenciar). Ele nunca aparece em leituras (GET). Guarde-o imediatamente em um lugar seguro; se você o perder, terá de rotacioná-lo.

curl

curl -sS -X POST "$TARNO_BASE_URL/v1/watch" \
  -H "X-API-Key: $TARNO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Cafeterias classe 43",
    "items": [{ "marca": "CAFE EXAMPLE" }],
    "callbackUrl": "https://seu-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: "Cafeterias classe 43",
    items: [{ marca: "CAFE EXAMPLE" }],
    callbackUrl: "https://seu-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();
// ⚠️ o signingSecret chega SOMENTE aqui — guarde-o agora, ele nunca reaparece.
await storeSecret(watchlist.id, watchlist.signingSecret); // "whsec_..."

Erros de criação:

  • 422 watchlist_too_large — você excedeu um limite (watchlists demais, ou itens demais em uma).
  • 422 unsafe_callback — o callbackUrl não passou na validação anti-SSRF.

Filtros

O objeto filters (jsonb) é opcional e todos os seus campos são opcionais:

Campo Tipo Efeito
minScore number (0..1) Piso de relevância. Sobrescreve o piso padrão da vigilância (0.7).
clase array\<integer> Classes de Niza a considerar.
allClasses bool Amplia uma vigilância por nro de volta a todas as classes (padrão é só a classe da marca).
liveOnly bool Exclui estados terminais (denegada, vencida, anulada).

Além disso, baseline: true na criação habilita o cruzamento baseline uma única vez contra todo o corpus histórico (D-14).

Gerenciar watchlists

Listar — GET /v1/watch

Escopo watch:read. Lista somente as suas (as da sua org). Nunca inclui o signingSecret.

curl -sS "$TARNO_BASE_URL/v1/watch" -H "X-API-Key: $TARNO_API_KEY"

Obter uma — GET /v1/watch/:id

Escopo watch:read. Uma watchlist inexistente ou de outra org retorna um 404 uniforme (não distingue "não existe" de "não é sua").

Atualizar — PATCH /v1/watch/:id

Escopo watch:write. Corpo esparso (sparse): envie apenas os campos que mudam.

Campo Tipo Notas
label string Renomeia a watchlist.
callbackUrl url | null Muda ou remove (null) o webhook.
filters objeto Substitui os filtros.
status 'active' | 'paused' Pausa/retoma a vigilância.
rotateSecret bool true → retorna um novo signingSecret uma vez na resposta.

Uma watchlist desconhecida ou de outra org → 404.

Aposentar — DELETE /v1/watch/:id

Escopo watch:write. É uma aposentadoria suave (status = 'retired'): interrompe os sweeps e o push, mas preserva os hits, que continuam legíveis por pull. Retorna 204 No Content.

Aposentar ≠ pausar. Pausar (status: 'paused' via PATCH) é reversível e apenas suspende a vigilância. Aposentar é DELETE (não um status que você possa enviar), é o fim de vida da watchlist, e ainda assim conserva o histórico de hits para consulta.

Entrega dual

Pull (sempre) — GET /v1/watch/:id/hits

Escopo watch:read. Sempre disponível, haja ou não callbackUrl.

Query Tipo Notas
since string Cursor opaco de keyset. Trate-o como caixa-preta.
limit integer Tamanho da página. Padrão 50, máximo 200.

Resposta: { items: WatchHit[], nextCursor: string | null }. nextCursor: null = fim — mesma semântica de keyset de primeiros passos. Para a próxima página, reenvie o cursor sem alterações como since.

curl -sS "$TARNO_BASE_URL/v1/watch/<id>/hits?limit=50" -H "X-API-Key: $TARNO_API_KEY"

Push (opcional) — webhook assinado

Se você registrou um callbackUrl, o Tarno faz um POST assinado para essa URL quando há novos hits. Verifique a assinatura (abaixo) e então faça o pull de /hits.

Verificação do webhook

O webhook é assinado com HMAC-SHA256. O header é:

X-Tarno-Signature: t=<unix>,v1=<hmac-hex>

A mensagem assinada é `${t}.${rawBody}` (o timestamp t, um ponto, e o corpo cru da requisição) usando o signing_secret (whsec_...) daquela watchlist. A tolerância anti-replay (skew) é de 300 s.

O webhook NÃO transporta hits (D-02 / D-11 — nunca corpos de resultados): é uma notificação. Ao recebê-lo, verifique a assinatura, rejeite se o skew exceder 300 s, e então faça o pull de GET /v1/watch/:id/hits.

Exemplo de verificação em Node (node:crypto):

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody = o corpo CRU exato (Buffer/string), sem 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;

  // Rejeita fora da janela 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);
}

Aviso: sempre verifique a assinatura e rejeite por skew antes de agir sobre a notificação.

Forma do hit

Cada elemento de GET /v1/watch/:id/hits é um WatchHit:

Campo Tipo Notas
id string Identificador do hit.
watchItemId string Item da watchlist que correspondeu.
watchlistId string Watchlist a que pertence.
matchedNro string Número de solicitação da marca detectada.
triggerHash string Hash de deduplicação do disparo.
score number Pontuação de relevância da correspondência.
signals objeto Sinais que explicam a correspondência.
hitKind 'new_filing' | 'update_on_prior' | 'baseline' Tipo de hit.
snapshot BrandSummary Snapshot congelado da marca detectada.
detectedAt string (data-hora) Quando foi detectado.

Os campos de snapshot são a mesma forma BrandSummary documentada em primeiros passos e em /docs.

MCP

REST é equivalente ao MCP: mesma chave, mesmos escopos, mesmos schemas de Zod. O servidor MCP expõe estas ferramentas de Vigilância:

Ferramenta 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

Veja o guia MCP para o endpoint, o transporte e a autenticação.

Erros

Todos os erros usam o envelope uniforme { error: { code, message, requestId } } — descrito em primeiros passos.

HTTP code Quando
401 unauthorized Chave ausente, malformada, desconhecida ou revogada.
403 forbidden Falta o escopo watch:* exigido, ou a chave não tem org dona.
404 not_found Watchlist inexistente ou de outra org (uniforme — não distingue os casos).
422 watchlist_too_large Você excedeu um limite (watchlists demais ou itens demais).
422 unsafe_callback O callbackUrl não passou na validação anti-SSRF.

Veja também