> ## Documentation Index
> Fetch the complete documentation index at: https://docs.signa.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Preview Watch

> Dry-run a query — match count without writing to the DB

## Overview

Returns the number of trademarks that would have alerted if this query had
been a live watch over the last `trial_window_days` (default 7). Uses the
same evaluation logic as a live watch, so the count is a faithful preview
\-- not a separate query engine.

Useful before [Create Watch](/api-reference/monitoring/watches/create) to
estimate volume and tune `score_threshold` for similarity watches.

<Note>
  Preview is a read-shaped operation (it creates nothing), so the
  `Idempotency-Key` header is **accepted but no longer required**. Sending one
  (as in the example below) is always safe -- the header is format-validated
  and otherwise ignored.
</Note>

## Body Parameters

<ParamField body="query" type="object" required>
  Same DSL as [Create Watch](/api-reference/monitoring/watches/create) --
  see [the canonical query reference](/guides/monitoring/watches#the-query-dsl).
  ID-bearing filters (`filters.trademarkIds`, `filters.ownerId`, ...) accept
  the same prefixed (`tm_*` / `own_*`) or raw-UUID forms as create.
</ParamField>

<ParamField body="trial_window_days" type="integer">
  Backtest window (1--365). Default 7.
</ParamField>

<ParamField body="count_only" type="boolean">
  Skip the matching marks and return just `estimated_match_count`. Default
  `false`, so the actual marks are returned by default. Set `true` for the
  cheap path when you only need the number (e.g. a live count as a watch is
  edited).
</ParamField>

<ParamField body="result_limit" type="integer">
  Page size for `results` (1--50). Default 20. Ignored when `count_only`.
</ParamField>

## Response

<ResponseField name="object" type="string">Always `"watch_preview"`.</ResponseField>
<ResponseField name="estimated_match_count" type="integer">Trademarks that would have alerted in the trial window.</ResponseField>

<ResponseField name="results" type="array">
  A page of the actual matching trademarks, in the **same summary shape as
  [search results](/api-reference/trademarks/list-trademarks)** (`GET
      /v1/trademarks` `data[]`). Returned by default; omitted when `count_only` is
  set. Up to `result_limit` items.
</ResponseField>

<ResponseField name="has_more" type="boolean">
  Whether more matches exist beyond the returned `results` page. Omitted when
  `count_only` is set.
</ResponseField>

<ResponseField name="result_limit" type="integer">
  Echo of the effective `results` page size. Omitted when `count_only` is set.
</ResponseField>

<ResponseField name="estimate_basis" type="string">
  **Present only when the count is an upper-bound estimate** rather than an
  exact count. The single value is `"candidacy_upper_bound"`, set by any of
  three triggers: the candidacy scan overflowed the server-side cap, the
  search backend was unreachable (the count falls back to the candidacy
  scan), or the server-side time budget expired after candidates were found
  (partial result). When the field is absent, the count is exact.
</ResponseField>

<ResponseField name="trial_window_days" type="integer">Echo of the requested window.</ResponseField>
<ResponseField name="request_id" type="string">Request identifier.</ResponseField>

## Latency and the time budget

Preview runs synchronously with a server-side time budget of roughly **20
seconds**. Typical latency: `class`, `mark`, and `owner` previews complete
in a few seconds; `similarity` previews over broad scopes and long windows
are the heaviest and can approach the budget. When the budget is exceeded
there are three cases:

1. **Zero candidates found in the trial window** -- normal `200` with
   `estimated_match_count: 0` (exact; never a timeout).
2. **Budget expired after candidates were found** -- `200` with a partial
   count: matches counted so far plus an upper bound for the unprocessed
   remainder, labeled `estimate_basis: "candidacy_upper_bound"`.
3. **Budget (or the database statement timeout) expired before any usable
   result existed** -- `504` with a `preview_timeout` error envelope and
   `retryable: false`. Retrying the same query will hit the same wall --
   narrow it instead (fewer offices, shorter `trial_window_days`, tighter
   filters).

## Errors

* **400** -- invalid `query` (see [Create Watch](/api-reference/monitoring/watches/create)).
* **413** -- `query` payload exceeds 256 KB.
* **429** -- another preview is already running for your organization (or
  the server's preview capacity is saturated). `rate_limited` envelope;
  honor the `Retry-After` header (roughly the server-side budget, \~20s)
  before retrying. The official SDK does this automatically. Unlike window
  `429`s (numeric `{limit};w={sec}` policies), these concurrency `429`s
  replace the policy header with a **named** policy --
  `RateLimit-Policy: "preview-concurrency";c=1;w=20` when your org's own
  preview holds the slot, or `"preview-concurrency-global";c=2;w=20` when
  the instance-wide cap is saturated -- where `c` is the concurrency cap,
  and the `RateLimit` header reads `remaining=0, reset=20`.
* **504** -- `preview_timeout` envelope (case 3 above). The body carries
  `retryable: false` -- do not blind-retry.

```json 504 preview_timeout theme={null}
{
  "error": {
    "type": "preview_timeout",
    "title": "Preview Timeout",
    "status": 504,
    "detail": "The preview could not produce a result within the server-side time budget.",
    "suggestion": "Narrow the watch query — add office or jurisdiction filters, reduce trial_window_days, or scope filters.trademarkIds — and retry.",
    "retryable": false,
    "retry_after": null
  },
  "request_id": "req_..."
}
```

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.signa.so/v1/watches/preview" \
    -H "Authorization: Bearer $SIGNA_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: preview-owner-watch-2026-06-12" \
    -d '{
      "query": {
        "version": "v1",
        "filters": { "ownerId": "own_018f9b2e-9b6c-7c9c-b4f1-1234567890ab" }
      },
      "trial_window_days": 30
    }'
  ```

  ```ts TypeScript theme={null}
  const preview = await signa.watches.preview({
    query: {
      version: 'v1',
      filters: { ownerId: 'own_018f9b2e-9b6c-7c9c-b4f1-1234567890ab' },
    },
    trial_window_days: 30,
  });
  console.log(`${preview.estimated_match_count} alerts in last 30d`);
  ```
</RequestExample>

<ResponseExample>
  ```json Exact count theme={null}
  {
    "object": "watch_preview",
    "estimated_match_count": 17,
    "trial_window_days": 30,
    "request_id": "req_..."
  }
  ```

  ```json Upper-bound estimate theme={null}
  {
    "object": "watch_preview",
    "estimated_match_count": 50000,
    "estimate_basis": "candidacy_upper_bound",
    "trial_window_days": 30,
    "request_id": "req_..."
  }
  ```
</ResponseExample>
