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 (
consumerIdnulo) não pode criar nem listar watchlists →403com 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 →
404uniforme.
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— ocallbackUrlnã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'viaPATCH) é reversível e apenas suspende a vigilância. Aposentar éDELETE(não umstatusque 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
- Autenticação e escopos —
X-API-Key,watch:read/watch:write, ciclo de vida da chave. - Conecte seu agente de IA (MCP) — as mesmas ferramentas de Vigilância via MCP.
- Primeiros passos — URL base, paginação por keyset, envelope de erro.
- A referência OpenAPI ao vivo em
/docs— o schema autoritativo por campo.