Skip to main content
A watch is a saved query that Signa runs on every ingestion sync. Pick the type whose required filter is the narrowest fit for what you want to track, build the query body, and use Preview to estimate volume before going live.
Creating, listing, and managing watches requires the portfolios:manage scope on your API key. The monitoring API is in beta: see Known beta limitations.

Pick a watch type

You want to…watch_typeRequired field
Track one specific mark you own (renewals, status drift).markfilters.trademarkIds (one ID)
Watch an entire portfolio of marks at once.portfoliofilters.trademarkIds (1 or more IDs)
Track a competitor by owner.ownerfilters.ownerId
Watch new filings in a Nice class (or class set), optionally by jurisdiction.classfilters.niceClasses
Detect confusingly-similar new filings.similarityq (text)
The query body uses the same vocabulary as trademark search, with one casing difference: REST search query params are snake_case (nice_classes=9), while watch-DSL filter keys are camelCase (niceClasses: [9]). If you can build the search, you can save it as a watch — just translate the key casing.

The query DSL

This is the canonical accepted shape — it is validated strictly, and anything outside it returns 400:
interface WatchQuery {
  version: 'v1';                       // REQUIRED — non-empty string
  q?: string;                          // keyword query (required for similarity)
  filters?: WatchFilters;              // camelCase keys — see the full list below
  trigger_events?: WatchTriggerEvent[]; // non-empty subset of the five events; default = first three
  score_threshold?: number;            // similarity only, JSON number 0..1
}

version (required)

Always "v1". Omitting it returns 400.

q keyword constraints

  • Whitespace-separated. Up to 20 keywords. Each keyword must be at least 3 characters.
  • Stop words are rejected with 400 to prevent watches that match too broadly. The current list is the, and, or, not, for, a, an, of, in, on, to, is.

filters — the full key list

Filter keys are camelCase. Unknown keys — including snake_case typos like nice_classes — are rejected with 400 (previously they were stored and silently ignored, making the watch broader than intended; the error message suggests the camelCase spelling when it recognizes the typo). Allowed keys: applicationNumber, attorneyFirmName, attorneyId, challengeStates, expiryDate, filingDate, filingRoute, firmId, goodsServicesText, hasMedia, hasProceedings, irNumber, isMadrid, isRetracted, isSeriesMark, jurisdictions, markFeatureType, markLegalCategory, niceClasses, office, offices, originOfficeCode, ownerCountry, ownerHasLei, ownerId, ownerLei, ownerName, ownerPubliclyTraded, ownerTicker, publicationDate, registrationDate, registrationNumber, renewalDueDate, rightKind, scopeKind, statusPrimary, statusReason, statusStage, terminationDate, trademarkIds, updatedAt, viennaCodes. Notes:
  • ID filters accept both forms. trademarkIds, ownerId, attorneyId, and firmId accept prefixed IDs (tm_*, own_*, att_*, firm_*) or raw UUIDs. Wrong-type or malformed IDs return 400 with the offending index in the field path.
  • Office codes are case-insensitivefilters.offices: ["USPTO"] is accepted and stored lowercase (["uspto"]). Each code must be a 2–10 character string that exists in GET /v1/offices.
  • filters.jurisdictions (e.g. ["US", "EU"]) is auto-translated to filters.offices at create time, so either spelling of scope works.

trigger_events

Any non-empty subset of these five values (anything else returns 400; an empty array is also rejected — omit the field to subscribe to the default set): Default set (applied when trigger_events is omitted):
  • trademark.created
  • trademark.updated
  • trademark.status_changed
Opt-in (valid, but only delivered when you list them explicitly):
  • trademark.retracted
  • trademark.corrected
Narrow the default set to silence events you don’t care about. For example, a portfolio watch that only fires on status changes:
trigger_events: ['trademark.status_changed']
Or subscribe to the default events plus retractions:
trigger_events: [
  'trademark.created',
  'trademark.updated',
  'trademark.status_changed',
  'trademark.retracted',
]
trademark.retracted / trademark.corrected are about the source feed, not legal status. A trademark.retracted alert fires on a pure is_retracted false → true flip — the upstream office feed pulled or retracted the record. trademark.corrected fires on the reverse true → false flip, when a previously-retracted record reappears (a source-side correction). These are distinct from a legal cancellation, which surfaces as trademark.status_changed (see the cancellation note below). Both are opt-in: a watch that omits trigger_events, or that lists only the default three, will never receive them — existing watches are unaffected.
Cancellation monitoring is built in. Whenever a matching mark’s status changes — including when it is cancelled, withdrawn, abandoned, or otherwise goes dead — the watch fires a trademark.status_changed alert. A cancellation is a status transition (the mark’s stage moves to cancelled and its primary status to inactive), so any mark, owner, portfolio, or class watch already covers it with no special setup. Subscribe to trademark.status_changed (or leave trigger_events at the default) to be alerted the moment a blocking mark is cancelled.

score_threshold (similarity only)

For similarity watches, alerts fire only when _score >= score_threshold. It must be a JSON number between 0 and 1 inclusive — a quoted string like "0.85" returns 400. The field is silently ignored for other watch types.
  • 0.7 is a reasonable starting point.
  • 0.85 is appropriate for production opposition workflows.
Use Preview to estimate volume at different thresholds before going live.

customer_reference — your own label

customer_reference is a top-level field on the watch (alongside name and query, not inside the query DSL) — a free-text passthrough string you control. Signa never interprets it; it’s there for your own correlation (a case number, internal watch ID, team name, routing key).
  • Set it on create, PATCH, or in a bulk create — max 200 characters. Pass null to clear it.
  • It’s returned on the watch resource (GET /v1/watches/{id}).
  • It is frozen at emit time and echoed onto every alert the watch produces, as data.customer_reference — on both the REST Alert resource and the alert.created webhook body. (Changing it later affects only alerts emitted after the change; already-emitted alerts keep the value they were stamped with.)
await signa.watches.create({
  name: 'Acme core mark -- status changes',
  watch_type: 'mark',
  customer_reference: 'matter-2026-0481',
  query: {
    version: 'v1',
    filters: { trademarkIds: ['tm_018f9b2e-9b6c-7c9c-b4f1-1234567890ab'] },
    trigger_events: ['trademark.status_changed'],
  },
});

Per-jurisdiction watches

Build separate watches per jurisdiction when you need different delivery cadence per region, or when different regional teams own different jurisdictions. Otherwise pass filters.jurisdictions: ["US", "EU", "GB"] once and let a single watch cover the set.
await signa.watches.create({
  name: 'Apple Inc -- class 9 worldwide',
  watch_type: 'owner',
  query: {
    version: 'v1',
    filters: {
      ownerId: 'own_018f9b2e-9b6c-7c9c-b4f1-1234567890ab',
      niceClasses: [9],
      jurisdictions: ['US', 'EU', 'GB', 'CA'],
    },
  },
});

Worked examples

Track one mark

await signa.watches.create({
  name: 'My core mark -- status changes',
  watch_type: 'mark',
  query: {
    version: 'v1',
    filters: { trademarkIds: ['tm_018f9b2e-9b6c-7c9c-b4f1-1234567890ab'] },
    trigger_events: ['trademark.status_changed'],
  },
});

Track a portfolio

await signa.watches.create({
  name: 'Q4 acquisitions portfolio',
  watch_type: 'portfolio',
  query: {
    version: 'v1',
    filters: {
      trademarkIds: [
        'tm_018f9b2e-9b6c-7c9c-b4f1-1234567890ab',
        'tm_018f9b2e-9b6c-7c9c-b4f1-1234567890ac',
        'tm_018f9b2e-9b6c-7c9c-b4f1-1234567890ad',
      ],
    },
  },
});

Track a Nice class

await signa.watches.create({
  name: 'New class-9 filings in US/EU',
  watch_type: 'class',
  query: {
    version: 'v1',
    filters: { niceClasses: [9], jurisdictions: ['US', 'EU'] },
    trigger_events: ['trademark.created'],
  },
});

Detect a similar mark

await signa.watches.create({
  name: 'Marks similar to ACME',
  watch_type: 'similarity',
  query: {
    version: 'v1',
    q: 'ACME',
    filters: { niceClasses: [9, 35] },
    score_threshold: 0.85,
  },
});

Preview before you launch

Dry-run the query against the last N days of data to estimate volume:
const preview = await signa.watches.preview({
  query: myQuery,
  trial_window_days: 30,
});
console.log(`${preview.estimated_match_count} alerts in the last 30 days`);
Preview uses the same evaluation logic as live watches, so the count is faithful. If you see thousands of matches, the query is too broad — tighten filters or raise score_threshold. Preview semantics worth knowing before you script against it (full details on Preview Watch):
  • estimate_basis. When the response carries estimate_basis: "candidacy_upper_bound", the count is an upper-bound estimate, not an exact match count (the scan hit the server-side cap, the search backend was unreachable, or the ~20s time budget expired partway). Absent field = exact count.
  • One preview at a time. A second concurrent preview for your org returns 429 with a Retry-After header (~20s). Honor it — the SDK does.
  • preview_timeout (504). If the time budget expires before any usable result exists you get a preview_timeout envelope with retryable: false. Narrow the query instead of retrying.
  • Latency. class / mark / owner previews complete in a few seconds; broad similarity previews are the heaviest and may approach the budget.
  • ID forms. Preview accepts the same prefixed (tm_* / own_*) or raw-UUID ID filters as create.

Create up to 100 at once

await signa.watches.bulk({ watches: [w1, w2, w3, /* ... */] });
The whole batch validates upfront. Partial failures don’t insert — all or nothing.

See what a watch would catch

To dry-run a watch before you create or update it, use POST /v1/watches/preview. By default it returns the actual matching marks (set count_only: true to get just the count). After you update a live watch (widen its offices, broaden trigger_events), the new criteria take effect automatically on the next ingestion sync — the watch re-evaluates with its current shape going forward. See Preview before you launch.

What’s not allowed in query

These are rejected with 400:
  • DSL/presentation keys (anywhere in the query, at any depth): function_score, script_score, script, sort, cursor, aggregations, aggs, highlight — scripting and presentation concerns don’t belong in a saved monitor.
  • query.match in any form — both the object form (match: {"nice_classes": [9]}) and the string form (match: "fuzzy"). Neither has ever been honored by the evaluator. Matching is driven by watch_type plus q / score_threshold (similarity) and the scoping fields under filters.
  • Unknown filters keys — including snake_case typos of valid keys (nice_classes instead of niceClasses). See the full key list.
  • Unknown trigger_events values (anything outside the five listed in trigger_events) and empty trigger_events arrays.

Common errors

StatusCause
400Invalid query DSL, stop words in q, more than 20 keywords, a forbidden key, or a missing required field for the selected watch_type.
409Your plan’s watch limit has been reached.
413query exceeds 256 KB.
See Create Watch for the full error schema.