June 2026
Watch preview: faster, bounded, and honest about estimates
POST /v1/watches/preview was rebuilt for reliability on heavy queries:
- The response may now carry
estimate_basis: "candidacy_upper_bound"when the count is an upper-bound estimate rather than an exact count (server cap overflow, search backend unreachable, or the time budget expiring after candidates were found). Absent field = exact count, as before. - Previews run under a ~20-second server-side budget. If the budget expires
before any usable result exists, the API returns a new
504with apreview_timeoutenvelope andretryable: false— narrow the query instead of retrying. - Concurrent previews are capped (one per organization at a time); excess
requests get
429with aRetry-Afterheader. - Prefixed IDs (
tm_*,own_*, …) inquery.filtersare now translated correctly on preview, matching create/update behavior.
Opposition windows now computed for pre-2024-published marks
Office holiday calendars (used for business-day rollover of opposition deadlines) cover 2024 onward. Previously, computing the opposition window for a mark published before that coverage began (roughly December 2023 and earlier) threw inside the deadline engine. That is fixed: when a window closed more than 14 days before the as-of date and the close date falls outside calendar coverage, the business-day roll is skipped — the status is provablyclosed regardless of holidays. Three call sites change behavior:
GET /v1/watches/{id}/diagnosticsno longer returns500for marks published before ~December 2023. It now returns the computed window withwindow_status: "closed".GET /v1/trademarks/{id}now returns a populatedopposition_windowfor pre-2024-published marks where it previously returnednull.- New alerts (and
alert.createdwebhook payloads) for such marks now carry a realmust_act_bydate and"closed"status where they previously carriednull.
window_status /
opposition_window_status is the meaningful field — treat the exact date as
approximate.
Per-endpoint-type rate limits are now enforced
A middleware-ordering bug meant every request was rate-limited against the generic read bucket regardless of endpoint type. This is fixed: the limiter now applies the per-endpoint-type limits your plan has always advertised (and whichGET /v1/organization/plan has always reported). The full
post-fix matrix for the beta plan:
| Endpoint type | Examples | Limit (per minute) |
|---|---|---|
search | GET/POST /v1/trademarks, /v1/owners, /v1/attorneys, /v1/firms, /v1/entities, /v1/suggest | 1,000 |
read | GET /v1/trademarks/{id} and other detail/list reads | 10,000 |
monitoring | /v1/watches, /v1/alerts, /v1/webhooks | 100 |
utility | /v1/organization/* | 1,000 |
reference | /v1/offices, /v1/classifications, /v1/jurisdictions, /v1/design-codes, /v1/event-types | 1,000 |
check | /v1/goods-services/suggest | 1,000 |
RateLimit-Policy / RateLimit response headers now reflect the limit
actually being enforced for each request. If your integration sustained more
than these rates, expect 429 responses with a Retry-After header —
respect it and back off (the official SDK does this automatically).
Usage endpoint: per-type rate_limits map
GET /v1/organization/usage now returns a rate_limits object with the
per-endpoint-type requests-per-minute matrix. The existing top-level
rate_limit field is deprecated — it only ever reflected the search-tier
limit, which made it disagree with the RateLimit-Policy header on
non-search endpoints. It is retained for backward compatibility. This
endpoint now also requires the billing:read scope, consistent with
GET /v1/organization/plan (keys created from the dashboard have it by
default).
Idempotency: preview and lookup no longer require a key
POST /v1/watches/preview and POST /v1/alerts/lookup are read-shaped
operations (they create nothing) and no longer require the
Idempotency-Key header. The header remains accepted — existing
integrations that send it are unaffected.
Webhooks: trademark_id added to alert.created
The alert.created webhook payload now carries trademark_id alongside
trademark_record_id (same value), matching the REST Alert resource’s field
name. trademark_record_id is deprecated and will be removed in v1.1 —
switch consumers to trademark_id.
Watch query DSL: strict validation
The watchquery DSL now rejects (HTTP 400) input it previously accepted
and silently ignored:
- Unknown
filterskeys — e.g. the snake_case typonice_classesinstead ofniceClassesused to no-op, silently broadening the watch. The error response lists the allowed keys. query.matchin any form — never honored by the evaluator. Usewatch_type: "similarity"withquery.q/query.score_thresholdfor similarity, andquery.filters.*for scoping.
filters.offices now accepts office codes in any casing and
normalizes them to lowercase (previously uppercase was rejected), matching
REST search behavior.
SDK 0.4.0
isRetryablenow honors the server’s explicitretryable: falseverdict in error envelopes — the SDK no longer auto-retries deterministic failures like the preview504 preview_timeout.AlertCreatedPayloadgainstrademark_id;trademark_record_idis marked deprecated (removal in v1.1).