Skip to main content

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 504 with a preview_timeout envelope and retryable: false — narrow the query instead of retrying.
  • Concurrent previews are capped (one per organization at a time); excess requests get 429 with a Retry-After header.
  • Prefixed IDs (tm_*, own_*, …) in query.filters are now translated correctly on preview, matching create/update behavior.
See Preview Watch for the full semantics.

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 provably closed regardless of holidays. Three call sites change behavior:
  • GET /v1/watches/{id}/diagnostics no longer returns 500 for marks published before ~December 2023. It now returns the computed window with window_status: "closed".
  • GET /v1/trademarks/{id} now returns a populated opposition_window for pre-2024-published marks where it previously returned null.
  • New alerts (and alert.created webhook payloads) for such marks now carry a real must_act_by date and "closed" status where they previously carried null.
Accepted imprecision: because the roll is skipped for these decades-past windows, the close date may land on a weekend or office holiday rather than the next business day. For long-closed windows, 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 which GET /v1/organization/plan has always reported). The full post-fix matrix for the beta plan:
Endpoint typeExamplesLimit (per minute)
searchGET/POST /v1/trademarks, /v1/owners, /v1/attorneys, /v1/firms, /v1/entities, /v1/suggest1,000
readGET /v1/trademarks/{id} and other detail/list reads10,000
monitoring/v1/watches, /v1/alerts, /v1/webhooks100
utility/v1/organization/*1,000
reference/v1/offices, /v1/classifications, /v1/jurisdictions, /v1/design-codes, /v1/event-types1,000
check/v1/goods-services/suggest1,000
The 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 watch query DSL now rejects (HTTP 400) input it previously accepted and silently ignored:
  • Unknown filters keys — e.g. the snake_case typo nice_classes instead of niceClasses used to no-op, silently broadening the watch. The error response lists the allowed keys.
  • query.match in any form — never honored by the evaluator. Use watch_type: "similarity" with query.q / query.score_threshold for similarity, and query.filters.* for scoping.
Additionally, 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

  • isRetryable now honors the server’s explicit retryable: false verdict in error envelopes — the SDK no longer auto-retries deterministic failures like the preview 504 preview_timeout.
  • AlertCreatedPayload gains trademark_id; trademark_record_id is marked deprecated (removal in v1.1).