Skip to main content
A watch is a saved query that Signa evaluates against every data update for the offices it covers. 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.

Quickstart: watch a class in the US

A complete path for the most common setup, “tell me about new USPTO filings in my Nice class.” Everything below needs an API key with the portfolios:manage scope.

1. Preview the volume

Scope to the US with filters.jurisdictions: ["US"] (equivalently filters.offices: ["US"]; jurisdictions are auto-translated to offices at create time):
Read estimate_basis before the count. When it is absent, as here, the count is exact: the US marks in class 9 that the watch below would have alerted on in the last 7 days. The number in this example is illustrative. trigger_events: ["trademark.created"] limits it to new filings, the same setting the watch uses; without it the preview also counts updates and status changes. On an office as busy as the USPTO, more than 10,000 marks matching your filters can change in a week. The preview then checks the first 10,000 and returns estimate_basis: "lower_bound": the real number is at least the count. Shorten trial_window_days or narrow the filters to get an exact number.

2. Create the watch

The response is the Watch object, save its id (wat_*). The watch is active immediately and is evaluated against every USPTO data update. Mutating calls like this one require the Idempotency-Key header.

3. Register a webhook (push)

Store the returned secret immediately, it is shown once. Each delivery is an HMAC-signed POST carrying the full alert object, whose data.alert.trademark.id you can pass straight to GET /v1/trademarks/{id}:
Verify signatures on every delivery (see Webhooks) and dedupe by the webhook-id header. No receiver yet? Use a tunnel or request-bin pattern to develop against real deliveries.

4. Add a polling fallback (pull)

Webhooks deliver in seconds but receivers go down. A periodic reconciliation pass over the watch’s alerts catches anything you missed:
Compare the returned alert IDs against what your receiver recorded, and fetch any gaps with POST /v1/alerts/lookup or process them directly from the list response. For the full reconciliation pattern, see Handling alerts.

Variations

  • US filings for a specific competitor: watch_type: "owner" with filters: { ownerId: "own_...", jurisdictions: ["US"] }.
  • Confusingly similar US filings: watch_type: "similarity" with version: "v3", q: "yourmark", similarity: ["identical", "fuzzy", "embedded", "phonetic"], min_tier: "similar", and filters: { jurisdictions: ["US"] }.
  • More jurisdictions later: PATCH the watch with a full replacement query, one watch covers the set. PATCH replaces the entire query object and re-validates it (version, the type’s required filters, trigger_events), so resend every field, not just the one you’re changing:

Pick a watch type

watch_type: "portfolio" is deprecated. It is still accepted, still evaluated and still alerts exactly as before, and there is no removal date, but it is no longer the direction we are building in. For marks you own the destination is a portfolio plus a webhook: membership is the rail that carries trademark.* and office_action.*, and unlike a watch it does not have to be rewritten every time the set of marks changes.Move before you delete. trademark.status_changed and office_action.issued are delivered for marks in a portfolio (see Event types in the webhooks guide). Add the marks to a portfolio, subscribe a webhook to those events, and retire the portfolio watch once your endpoint is receiving the direct events.Keep using mark, similarity and owner watches for alerts on marks you do not own. Nothing replaces those.
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, validated strictly, anything outside it returns 400:

version (required)

Required: "v1", "v2" and "v3" are accepted. Omitting version returns 400. Send "v3". Every query you create, bulk create, preview or change with PATCH is stored in the v3 form, whatever version it sends: it uses the similarity channels and the min_tier gate. So { "version": "v3", "q": "acme" } is stored as { "version": "v3", "q": "acme", "similarity": ["identical", "fuzzy", "embedded", "lookalike"], "min_tier": "related" }, and a filter-only query gains "min_tier": "related" (without q every tier passes, so it matches as before). The one exception is a query with the deprecated strategies or min_match_tier keys, which is kept in the legacy form (see Watches created before 2026-09-29).

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 (the error message suggests the camelCase spelling when it recognizes the typo). Allowed keys: applicationNumber, attorneyFirmName, attorneyId, challengeStates, entityGroup, entityId, 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, and so does an ID that names no trademark, owner, attorney or firm.
  • Merged owners, attorneys and firms. The stored watch always keeps the ID you saved. Following a merge to the record a duplicate was merged into ships disabled and is being rolled out; until then a watch matches the marks of the ID it names. Diagnostics names a merge in its no-match reason, with merge follow not enabled while it is off.
  • niceClasses are classes 1 to 45. A numeric string such as "25" is accepted and stored as the number 25. Any other value (0, 46, "abc") returns 400 naming the entry, for example query.filters.niceClasses[1]. A watch saved before this check with such a value keeps working, shows invalid_filter in its health, and accepts a PATCH that sends the stored value back unchanged. Responses always echo these filters in the prefixed form regardless of which form you sent, so a GET → edit → PATCH round-trip is lossless.
  • Office codes are case-insensitive: filters.offices: ["uspto"] is accepted and echoed back in the uppercase WIPO ST.3 form (["US"]); legacy lowercase codes are accepted permanently as aliases. 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. Watches keep office-based scoping and do not apply the protection-scope jurisdiction expansion used by search and screening, so a watch scoped to ["DE"] watches the German office only and does not surface EU trade marks.
  • entityId / entityGroup accept ent_* ids or raw UUIDs and are mutually exclusive. See Entity watches.
  • Date filters follow the trademark fields. expiryDate and renewalDueDate match the office-published expiry_date and renewal_due_date, exactly like the search filters, so records whose office publishes no expiry date (every USPTO record) never match an expiryDate range. Watches created before the provenance release now use these semantics too.
  • ownerTicker and ownerPubliclyTraded are subsidiary-inclusive. They share the same public-company/listing fields as GET /v1/trademarks, so a watch with ownerTicker: "NKE" fires on Nike’s own filings and those of its subsidiaries (an owner’s entity listing ticker, direct or inherited from a listed ancestor, folds into this field). ownerPubliclyTraded: true matches any owner with an active listing association (a confirmed SEC ticker, or a resolved entity that is listed or a subsidiary of a listed company); a false or absent value means no confirmed listing, not confirmed private.

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
trademark.updated needs a change you can see on the trademark resource: an update that only moves Signa-computed values (a computed expiry or renewal date recomputed, with no office-stated date on either side) raises no alert. See the v2 values. Known limitation (IR renewals): an international registration’s expiry_date is the expiry WIPO published on its designations. When WIPO publishes a renewal, the designations change but the IR’s own record does not, so a watch on the IR’s tm_ id raises no trademark.updated even though its expiry_date moved. Narrow the default set to silence events you don’t care about. For example, a portfolio watch that only fires on status changes:
Or subscribe to the default events plus retractions:
Each triggered event reaches you as an alert.created webhook delivery carrying the trademark event type in data.alert.event.type. See The five trademark event types for the delivery-side view (envelope shape, the pure-flip rule for retracted / corrected, and scope caveats).
Retractions, corrections, and cancellations, in one paragraph. trademark.retracted fires when the source office feed pulls or retracts a record (is_retracted flips false to true); trademark.corrected fires on the reverse flip, when a previously retracted record reappears. It also fires when Signa corrects a status it published wrongly (for example a mark shown as expired that the office records as renewed), or when office records that reached Signa late show a mark lapsing or a dead mark returning. Such a correction arrives as trademark.corrected instead of trademark.status_changed or trademark.updated, so it never reaches a watch that did not opt in. (For marks in a portfolio, a webhook subscribed to trademark.status_changed receives it directly as trademark.corrected without opting in.) Any other change Signa stores with a correction or from late office records, including marks Signa first learns of from late records, raises no alert. A change that only relabels where a status comes from (its basis) never raises an alert. Both are opt-in: list them explicitly in trigger_events, or they never arrive. A legal cancellation is different and needs no opt-in: whenever a matching mark’s status changes, including moving to cancelled, withdrawn, abandoned, or otherwise going dead, the watch fires a regular trademark.status_changed alert (subscribed by default). Any mark, owner, portfolio, or class watch already covers cancellations with no special setup.

similarity (similarity watches)

The search channels that run q, the same channels as GET /v1/trademarks: Omit similarity for the default: identical, fuzzy, embedded, lookalike. identical is added if you leave it out, and the stored query lists the resolved set. An empty array returns 400 (too_short), an unknown channel returns 400 (invalid_value), and similarity without q returns 400 (conflicting_params).

min_tier (similarity watches)

The weakest similarity tier that fires an alert. Tiers run from strongest to weakest: Tiers are retrieval categories, not measured edit distances. Omit min_tier for the default, related, which lets every tier through. min_tier: "near_identical" alerts on identical and near_identical matches only. A mark found only by its trademark number in q has no tier and always passes the gate. See match tiers for how search assigns them. Each alert reports the tier it matched at as match.tier:
match.score is the engine score for the watch query. It is not the 0 to 100 search relevance_score, so don’t compare the two. Use Preview to estimate volume for different similarity / min_tier combinations before going live.

Watches created before 2026-09-29

Stored watches are never rewritten. A watch saved in the legacy form (created before the v3 query form, or written with the deprecated keys) keeps its exact matching: its stored strategies lane set and its min_match_tier gate. Diagnostics reports min_match_tier for them, with similarity_applied and min_tier set to null. Their alerts still carry match.tier as a label; it does not gate them. A legacy watch moves to the new matching when you PATCH a changed query without the deprecated keys, even if you only change q: the changed query is a re-save and is stored in the v3 form. Sending the stored query back unchanged on PATCH changes nothing, so a GET then PATCH round trip never migrates a watch. strategies and min_match_tier are deprecated until 2026-12-28. On create, update, bulk create and preview:
  • A query that uses either key is kept as a legacy watch until it is re-saved without them. It is stored as sent and keeps the legacy matching: the strategies lane set and the min_match_tier gate. Nothing is mapped to similarity or min_tier, because no mapping keeps every alert the legacy lanes find.
  • The response carries a top-level deprecations array naming each replacement and the sunset, for example [{ "param": "query.strategies", "replacement": "query.similarity", "sunset": "2026-12-28" }].
  • An old key together with any new key (similarity, min_tier or version: "v3") returns 400 (conflicting_params).
From 2026-12-28 either old key returns 400 with code unknown_parameter, naming its replacement. The migration guide maps the old values to the new ones.

delivery_mode: per-alert delivery today

delivery_mode is a top-level field on the watch (alongside name and query). Only always_per_alert is accepted on create and update today. Any other value returns 400. The digest modes (digest_above_threshold, digest_only) appear in the watch schema and the field is stored to eventually drive digest batching, but digest suppression is not live: every watch delivers one alert.created webhook per alert, regardless of the stored mode. Until digest modes ship, size your receiver for per-alert volume (Preview estimates it) and batch on your side if you need digests: dedupe by webhook-id and roll alerts up on your own schedule. See Known beta limitations.

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 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.

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.

Entity watches

An entity (ent_* id, from GET /v1/entities) groups the many per-office owner records that belong to one real-world company. Entity watches let you say “watch everything Unilever does” without enumerating owner records, and without re-editing the watch when Signa’s entity resolution links new owner records to the company later. Two scopes, mutually exclusive (sending both returns 400):
  • filters.entityId: the entity’s own marks: every trademark whose current owners are linked to that entity, across all offices. The target must be a linked entity: an owner id written with the entity prefix (ent_<owner-uuid>, which older API versions returned for unlinked owners) is rejected with 400 (synthetic_entity_id). To watch one owner record that is not linked to an entity, use filters.ownerId (own_*).
  • filters.entityGroup: the entity’s whole GLEIF corporate family: the ultimate parent plus all subsidiaries. Pass any family member’s id; Signa climbs to the family root and covers every entity in the tree.
Semantics worth knowing:
  • Accepted id forms. ent_<uuid> or a raw UUID (stored normalized). Wrong-prefix or malformed ids return 400, and so does an owner id with the entity prefix: the error names the replacement, the linked entity’s id when the owner has since been linked, otherwise filters.ownerId. For watch_type: "owner", entityId or entityGroup satisfies the required-field rule in place of ownerId.
  • Existing watches were migrated. Watches created before the provenance release that targeted an owner id with the entity prefix were rewritten to filters.ownerId (or to the entity the owner has since been linked into), and keep evaluating the same marks.
  • Validated at create time. A nonexistent or suppressed entity returns 400 (the watch would never fire, so we reject it instead of storing a dead watch). A corporate family too large to traverse (more than 5,000 related entities or deeper than 32 levels) returns 422 with type entity_too_large and reason: "family_graph_too_large". Watch a specific entity in the family with entityId instead.
  • Freshness is automatic. Matching happens against each trademark’s live entity linkage, refreshed continuously by Signa’s entity-resolution pipeline. When a newly discovered subsidiary (or a newly linked owner record) is attached to the entity, its marks start matching your watch on their next change, with no PATCH needed. entityGroup re-resolves the family on every evaluation, so family growth is picked up automatically. If the entity you watch is later fused into another entity, the watch follows the surviving entity on its own.
  • No member cap. Entity watches, like ?entity_id= on trademark search, have no member-owner limit: even the largest portfolios are watchable. Only the family-graph traversal bound above applies, and only to entityGroup.
  • Honest caveat: coverage depends on entity-resolution linkage. An entity watch only sees marks whose owner records have been linked to the entity. Owner records that entity resolution hasn’t linked yet, common for name variants, recent filings, or offices with sparse identifiers, won’t match until they’re linked. For belt-and-braces coverage of a specific known owner record, add a separate ownerId watch.

Worked examples

Track one mark

Track a fixed set of marks (deprecated)

A portfolio plus a webhook subscribed to trademark.status_changed / office_action.issued replaces this for marks you own (see the webhooks guide’s Event types table). Keep the watch until your endpoint receives the direct events; see the note above.

Track a Nice class

Detect a similar mark

Preview before you launch

Dry-run the query against the last N days of data to estimate volume:
Preview uses the same matching logic as live watches. The count is exact only when the response has no estimate_basis; otherwise it is a bound (see below). Every filter applies, including on a busy office: when the window holds more changes than the preview checks one by one, it searches for the marks matching your filters instead, the same way it handles a text query. For a watch with a text query, if you see thousands of matches, the query is too broad, tighten filters, drop channels from similarity, or raise min_tier to a stronger tier (identical is the narrowest). The response echoes the channels it ran as similarity_applied and the gate as min_tier. By default it returns the actual matching marks; set count_only: true to get just the count. Preview semantics worth knowing before you script against it (full details on Preview Watch):
  • estimate_basis. Absent means an exact count. lower_bound: more than 10,000 marks matched the query or filters in the window, or the search did not finish; the real number is higher. query_upper_bound: the time budget ran out before the change check; the real number is lower. candidacy_upper_bound: a watch with no text query and no filter beyond offices or trademark IDs changed more marks than the preview counts one by one, the search backend was unreachable, or the time budget ran out; the count is the changed marks in scope. See Preview Watch.
  • One preview at a time. A second concurrent preview for your org returns 429 with a Retry-After header. 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. The budget is about 20 seconds, with each database read capped at 10 seconds. mark and owner previews and narrow class previews complete in a few seconds; broad similarity previews are the heaviest and may approach the 20 second budget. A broad watch without a text query (for example one class in US over 30 days) can hit the 10 second read cap and return preview_timeout after about 10 seconds.
  • ID forms. Preview accepts the same prefixed (tm_* / own_*) or raw-UUID ID filters as create, and rejects the same unknown owner, attorney or firm IDs and Nice classes outside 1-45 with 400.
After you update a live watch (widen its offices, broaden trigger_events), the new criteria take effect automatically on the next data update, the watch re-evaluates with its current shape going forward.

Create up to 100 at once

The whole batch validates upfront. Partial failures don’t insert, it’s all or nothing. When items fail, the 400 lists every failing item, and each errors[].field starts with the item’s index: watches[2].query.q.

Health and proof of monitoring

Every watch carries an honest health block so that “no alert” is provably distinct from “not looking”. GET /v1/watches/{id} returns the full object; GET /v1/watches returns just health_status per row (the full object would cost an extra query per row).

Health states

The watch-level status is the worst state across its in-scope offices. Precedence, highest first: paused, unsupported, degraded, lagging, pending, healthy. issues at the watch level is the deduplicated set of the per-office issue slugs, plus invalid_filter when the stored niceClasses include a value outside 1-45. Remove that value with PATCH.
coverage_through is office-data time, not wall-clock. It is the timestamp of the office’s own published data that the watch has been evaluated through, frozen at evaluation time. It never advances just because time passed. last_evaluated_at is the separate wall-clock instant of the last evaluation. When an office lags, coverage_through stops advancing while last_evaluated_at may keep moving. coverage_basis tells you how strong the claim is: source_dates (strongest, the office’s own published dates) beats date_range, which beats run_completed (weakest, “the data available to us as of run completion”).
Health certifies that evaluation occurred against the stated office-data horizon. It does not certify that every relevant mark was surfaced. What a watch catches is bounded by its query and similarity channels, and by connector coverage. Health is proof that we looked, and of the data horizon we looked through, not a guarantee of exhaustive recall.

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"). Matching is driven by watch_type plus q / similarity / min_tier (similarity) and the scoping fields under filters.
  • Search text filters and exclusions: mark_text and every mark_text_* key, exclude, entity_id_not, entity_group_not, owner_id_not, trademark_ids_not. Watches don’t support them yet; each returns 400 with code unsupported and field query.<key>. Watch the text with q and similarity, narrow with filters, or run the filtered search with GET /v1/trademarks.
  • 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.
  • Dead filter values: a niceClasses entry that is not a class from 1 to 45 (numeric strings such as "25" are accepted as numbers), and an ownerId, attorneyId or firmId that is not an ID string, names no record, or names an owner Signa has retired.

Common errors

See Create Watch for the full error schema.