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

# Watch Diagnostics

> Explainable trace for a single (watch, trademark) pair — why an alert did or didn't fire

## Overview

Self-service debugging for the question every monitoring customer eventually
asks: **"I expected an alert for this trademark. Why didn't I get one?"**

Returns an 11-field, fully-explained trace of how Signa's evaluator saw the
trademark relative to this watch, walking from candidacy through trigger-event
filtering through match outcome through delivery-mode resolution. When an alert
*was* fired, the same response surfaces the cross-system trace IDs you need to
correlate against your webhook receiver logs.

Read-only. The endpoint never writes; it surfaces fields the evaluator and
dispatchers already persisted.

Required scope: `portfolios:manage`.

## Path Parameters

<ParamField path="id" type="string" required>
  Watch ID (`wat_*`).
</ParamField>

## Query Parameters

<ParamField query="trademark_id" type="string" required>
  Trademark ID (`tm_*`) to evaluate against this watch. Cross-org IDs return
  404 (info-disclosure prevention).
</ParamField>

## Response

<ResponseField name="watch_id" type="string">Echoed watch ID (`wat_*`).</ResponseField>
<ResponseField name="trademark_id" type="string">Echoed trademark ID (`tm_*`).</ResponseField>
<ResponseField name="office_code" type="string">Office that issued the trademark (lowercase ISO-style code, e.g. `uspto`, `euipo`).</ResponseField>

<ResponseField name="evaluated" type="boolean">
  `true` if the watch evaluator processed this trademark in the current
  retention window. `false` either because the trademark wasn't in candidacy,
  or because the relevant provenance has aged out of the retention window
  (see `data_window` below). When false, `reason` explains why.
</ResponseField>

<ResponseField name="office_in_scope" type="boolean">
  `true` if this watch's filters include this trademark's office. Watch +
  office routing happens via the office-checkpoint state; if the checkpoint
  is missing or marked superseded, this is `false`.
</ResponseField>

<ResponseField name="candidacy_passed" type="boolean">
  `true` if a change record exists for this trademark within the
  retention window. Candidacy is a prerequisite for evaluation; if no change
  was recorded the evaluator never saw the mark.
</ResponseField>

<ResponseField name="trigger_event_type" type="string | null">
  The lifecycle event derived for the matched change (`trademark.created`,
  `trademark.updated`, `trademark.status_changed`, etc.). When an alert was
  fired this is the frozen event type from the alert record; otherwise
  it's derived from the most recent change record.
</ResponseField>

<ResponseField name="trigger_event_in_filter" type="boolean">
  `true` if `trigger_event_type` is allowed by the watch's
  `query.trigger_events` filter. When false, the evaluator silently dropped
  the candidate — that's the answer to "why no alert".
</ResponseField>

<ResponseField name="opensearch_score" type="number | null">
  The search relevance score at evaluation time, when applicable.
  Currently always `null` — the evaluator does not persist the per-trademark
  score on the alert row. Documented gap; will populate on a future schema bump.
</ResponseField>

<ResponseField name="score_threshold" type="number | null">
  The watch's configured `query.score_threshold` (0..1) for similarity
  watches. `null` for non-similarity watches.
</ResponseField>

<ResponseField name="alert_fired" type="boolean">
  `true` if an alert record exists for this (watch, trademark) pair.
  When true, `alert_id` is the cross-system trace handle.
</ResponseField>

<ResponseField name="reason" type="string">
  Human-readable explanation for the outcome. Walked in this order:

  1. `"alert fired"` — an alert row exists.
  2. `"watch does not include office {code}"` — checkpoint missing/superseded.
  3. `"trademark evaluated more than {N} days ago; provenance no longer
     available"` — past the diagnostic freshness horizon.
  4. `"trademark not in candidacy window for the most recent {office} sync
     run"` — no recent change record.
  5. `"trigger event {type} not in watch.trigger_events"` — change exists
     but the event type is filtered out.
  6. `"score {n} below threshold {t}"` — similarity match below threshold
     (only fires when both are populated).
  7. `"would alert but rolled into digest"` — the watch's effective
     delivery mode resolved to digest; alert is queued for the digest job.
  8. `"no matching reason available"` — fallback; surface to support if
     you see it.
</ResponseField>

<ResponseField name="delivery_mode_effective" type="'per_alert' | 'digest' | null">
  Resolved delivery mode using the same logic as the evaluator. For
  `digest_above_threshold` this depends on the trailing 24h alert volume,
  so the value is "current effective" not "effective at evaluation time" —
  documented in the API contract. Since only `always_per_alert` watches can
  be created today (digest modes are v1.1), in practice this is
  `per_alert` or `null`.
</ResponseField>

<ResponseField name="lease_state" type="'held' | 'released' | 'abandoned' | 'epoch_raced' | 'cas_lost' | null">
  Best-effort classification of the per-(watch, office) evaluator lease.
  `held` is unambiguous (active lease started \< 15min ago);
  `epoch_raced` indicates the evaluator's last-seen epoch lags behind
  the watch's current epoch (replay race); `released` is the
  default post-iteration state. `abandoned` / `cas_lost` are heuristic
  hints, not guarantees.
</ResponseField>

<ResponseField name="evaluation_epoch" type="integer">
  The watch's current `evaluation_epoch`. Bumped internally when a watch is
  re-evaluated (epoch=0 for a watch that has never been re-evaluated).
</ResponseField>

<ResponseField name="replay_epoch_origin" type="integer | null">
  When the alert was emitted under a bumped epoch, this is the alert's
  frozen `evaluation_epoch`. `null` when the watch's epoch is 0 or no alert
  exists.
</ResponseField>

<ResponseField name="last_relevant_sync_run" type="object | null">
  The ingestion run referenced by the watch+office checkpoint's
  `last_evaluated_sync_run_id`.

  <Expandable title="last_relevant_sync_run">
    <ResponseField name="id" type="string">Operational identifier of the ingestion run (global `sync_runs` UUID — not an org-scoped reference).</ResponseField>
    <ResponseField name="office_code" type="string">Office that produced the run.</ResponseField>
    <ResponseField name="completed_at" type="string | null">When ingestion finished.</ResponseField>
    <ResponseField name="search_indexed_at" type="string | null">When the search index drained downstream.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="alert_id" type="string | null">
  Public ID (`alt_*`) of the alert that fired. Cross-reference against
  `alert_id` on delivery audit rows under
  [GET /v1/webhooks/{id}/deliveries](/api-reference/monitoring/webhooks/list-deliveries),
  or retrieve the alert directly via `GET /v1/alerts/{id}`.
  `null` when no alert fired.
</ResponseField>

<ResponseField name="opposition" type="object | null">
  Computed opposition-window state for this trademark. `null` when the
  trademark has no `publication_date_first` or no rule applies for the
  jurisdiction/route.

  <Expandable title="opposition">
    <ResponseField name="must_act_by" type="string | null">ISO date — last day to file. Prefers the alert's frozen value when an alert exists; otherwise computed live from the deadline-rules engine.</ResponseField>
    <ResponseField name="rule_source" type="string | null">Human-readable rule source name (e.g. "USPTO TMEP §1503.02").</ResponseField>
    <ResponseField name="rule_version" type="string | null">Rule registry version.</ResponseField>
    <ResponseField name="window_status" type="'open' | 'closed' | 'not_started' | 'unknown' | null">Coarse status. `closing_soon` / `critical` from alerts collapse to `open` here.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="data_window" type="object">
  Retention horizons for the data this endpoint reads. Past these horizons
  the corresponding fields cannot be reconstructed and `reason` will
  surface "provenance no longer available".

  <Expandable title="data_window">
    <ResponseField name="trademark_changes_retention_days" type="integer">90</ResponseField>
    <ResponseField name="outbox_retention_days" type="integer">7</ResponseField>
    <ResponseField name="indexing_status_retention_days" type="integer">14</ResponseField>
    <ResponseField name="deliveries_retention_days" type="integer">30 — applies to delivery audit records.</ResponseField>
    <ResponseField name="alerts_retention_days" type="integer">90</ResponseField>
    <ResponseField name="diagnostic_freshness_horizon_days" type="integer">90 — the smallest of the windows that power the candidacy fields.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="request_id" type="string">Request identifier.</ResponseField>

## Errors

* **400** — `trademark_id` query parameter missing.
* **403** — caller lacks `portfolios:manage`.
* **404** — watch does not exist, belongs to another org, the trademark
  does not exist, or this trademark is out of the watch's office scope and
  no alert was ever produced for the pair. The endpoint deliberately does
  not distinguish between these cases (info-disclosure prevention).

## Retention

Change records are retained for 90 days; delivery audit records for 30 days.
Diagnostics queries past the 90-day horizon return `evaluated=false`
with `reason` explaining the freshness limit.

## Authentication

Standard API key via `Authorization: Bearer $SIGNA_API_KEY`. Org-scoped:
the watch must belong to the calling org.

<RequestExample>
  ```bash cURL theme={null}
  curl "https://api.signa.so/v1/watches/wat_01HK7M.../diagnostics?trademark_id=tm_01HK7N..." \
    -H "Authorization: Bearer $SIGNA_API_KEY"
  ```

  ```ts TypeScript theme={null}
  const trace = await signa.watches.diagnostics('wat_01HK7M...', {
    trademarkId: 'tm_01HK7N...',
  });
  console.log(trace.reason);
  ```
</RequestExample>

## See also

* [Troubleshooting guide](/guides/monitoring/troubleshooting) — walkthrough of
  the diagnostics → webhook-deliveries debug flow.
* [List webhook deliveries](/api-reference/monitoring/webhooks/list-deliveries)
  — pair `alert_id` from this response with the delivery audit log
  to confirm webhook receipt.
