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 theportfolios:manage scope.
1. Preview the volume
Scope to the US withfilters.jurisdictions: ["US"] (equivalently filters.offices: ["US"]; jurisdictions are auto-translated to offices at create time):
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
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)
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}:
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: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"withfilters: { ownerId: "own_...", jurisdictions: ["US"] }. -
Confusingly similar US filings:
watch_type: "similarity"withversion: "v3",q: "yourmark",similarity: ["identical", "fuzzy", "embedded", "phonetic"],min_tier: "similar", andfilters: { jurisdictions: ["US"] }. -
More jurisdictions later:
PATCHthe watch with a full replacementquery, one watch covers the set.PATCHreplaces the entirequeryobject 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
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 returns400:
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
400to prevent watches that match too broadly. The current list isthe, 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, andfirmIdaccept prefixed IDs (tm_*,own_*,att_*,firm_*) or raw UUIDs. Wrong-type or malformed IDs return400with 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, withmerge follow not enabledwhile it is off. niceClassesare classes 1 to 45. A numeric string such as"25"is accepted and stored as the number25. Any other value (0,46,"abc") returns400naming the entry, for examplequery.filters.niceClasses[1]. A watch saved before this check with such a value keeps working, showsinvalid_filterin its health, and accepts aPATCHthat sends the stored value back unchanged. Responses always echo these filters in the prefixed form regardless of which form you sent, so aGET→ edit →PATCHround-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 inGET /v1/offices. filters.jurisdictions(e.g.["US", "EU"]) is auto-translated tofilters.officesat 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/entityGroupacceptent_*ids or raw UUIDs and are mutually exclusive. See Entity watches.- Date filters follow the trademark fields.
expiryDateandrenewalDueDatematch the office-publishedexpiry_dateandrenewal_due_date, exactly like the search filters, so records whose office publishes no expiry date (every USPTO record) never match anexpiryDaterange. Watches created before the provenance release now use these semantics too. ownerTickerandownerPubliclyTradedare subsidiary-inclusive. They share the same public-company/listing fields asGET /v1/trademarks, so a watch withownerTicker: "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: truematches 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); afalseor 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.createdtrademark.updatedtrademark.status_changed
trademark.retractedtrademark.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:
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 thev3 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
strategieslane set and themin_match_tiergate. Nothing is mapped tosimilarityormin_tier, because no mapping keeps every alert the legacy lanes find. - The response carries a top-level
deprecationsarray 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_tierorversion: "v3") returns400(conflicting_params).
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. Passnullto 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_referenceon both the RESTAlertresource and thealert.createdwebhook 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 passfilters.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 with400(synthetic_entity_id). To watch one owner record that is not linked to an entity, usefilters.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.
- Accepted id forms.
ent_<uuid>or a raw UUID (stored normalized). Wrong-prefix or malformed ids return400, 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, otherwisefilters.ownerId. Forwatch_type: "owner",entityIdorentityGroupsatisfies the required-field rule in place ofownerId. - 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) returns422with typeentity_too_largeandreason: "family_graph_too_large". Watch a specific entity in the family withentityIdinstead. - 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.
entityGroupre-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 toentityGroup. - 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
ownerIdwatch.
Worked examples
Track one mark
Track a fixed set of marks (deprecated)
A portfolio plus a webhook subscribed totrademark.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: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
429with aRetry-Afterheader. Honor it, the SDK does. preview_timeout(504). If the time budget expires before any usable result exists you get apreview_timeoutenvelope withretryable: false. Narrow the query instead of retrying.- Latency. The budget is about 20 seconds, with each database read capped at 10 seconds.
markandownerpreviews and narrowclasspreviews complete in a few seconds; broadsimilaritypreviews are the heaviest and may approach the 20 second budget. A broad watch without a text query (for example one class inUSover 30 days) can hit the 10 second read cap and returnpreview_timeoutafter 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 with400.
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
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 honesthealth 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-levelstatus 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”).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.matchin any form, both the object form (match: {"nice_classes": [9]}) and the string form (match: "fuzzy"). Matching is driven bywatch_typeplusq/similarity/min_tier(similarity) and the scoping fields underfilters.- Search text filters and exclusions:
mark_textand everymark_text_*key,exclude,entity_id_not,entity_group_not,owner_id_not,trademark_ids_not. Watches don’t support them yet; each returns400with codeunsupportedand fieldquery.<key>. Watch the text withqandsimilarity, narrow withfilters, or run the filtered search withGET /v1/trademarks. - Unknown
filterskeys, including snake_case typos of valid keys (nice_classesinstead ofniceClasses). See the full key list. - Unknown
trigger_eventsvalues (anything outside the five listed intrigger_events) and emptytrigger_eventsarrays. - Dead filter values: a
niceClassesentry that is not a class from 1 to 45 (numeric strings such as"25"are accepted as numbers), and anownerId,attorneyIdorfirmIdthat 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.