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

# Handling alerts

> Pull, push, deduplication, and reconciliation

You have three ways to receive alerts, in increasing operational complexity:

1. **Polling.** A scheduled job calls [`GET /v1/alerts`](/api-reference/monitoring/alerts/list) and walks pages until it sees an `id` you've already stored.
2. **Webhook.** Register an endpoint via [`POST /v1/webhooks`](/api-reference/monitoring/webhooks/create) and let Signa push alerts to you.
3. **Webhook + reconciliation.** Push for low latency, then periodically call [`POST /v1/alerts/lookup`](/api-reference/monitoring/alerts/lookup) with the IDs you've persisted to confirm nothing was lost during a receiver outage.

Pure polling is fine for low-volume internal tools. For production external workflows, prefer webhook + reconciliation.

## Deduplicate by alert ID

Alerts are immutable, but the same `alert.created` event can arrive at your endpoint multiple times across retries and redeliveries. Use the `webhook-id` header (which equals the alert's prefixed ID, `alt_*`) as your idempotency key.

```ts theme={null}
const alertId = req.header('webhook-id')!;
if (await alreadyProcessed(alertId)) {
  return res.sendStatus(200); // ack but skip
}
// Record processing and execute the side effect in the same transaction
// so a crash between them leaves the alert unprocessed (and retriable),
// not silently "done".
await db.transaction(async (tx) => {
  await markProcessed(tx, alertId);
  await handle(tx, JSON.parse(req.body));
});
return res.sendStatus(200);
```

If your side effect cannot share a transaction with your dedup store (for example, a third-party API call), use a two-stage `processing -> done` state and only flip to `done` after the side effect returns success.

## Polling pattern

```ts theme={null}
let cursor: string | undefined;
while (true) {
  const page = await signa.alerts.list({ limit: 100, cursor });
  for (const alert of page.data) {
    if (await alreadyProcessed(alert.id)) {
      // Caught up, older alerts have already been processed.
      return;
    }
    await markProcessed(alert.id);
    await handle(alert);
  }
  if (!page.has_more) return;
  cursor = page.pagination.cursor!;
}
```

The `GET /v1/alerts` endpoint accepts:

| Parameter    | Type                             | Notes                                                                                                                                                  |
| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `limit`      | number                           | 1-100, default 20.                                                                                                                                     |
| `cursor`     | string                           | Opaque cursor from the previous response.                                                                                                              |
| `severity`   | `normal` \| `high` \| `critical` | Filter by severity.                                                                                                                                    |
| `event_type` | string                           | One of the five alert event types: `trademark.created`, `trademark.updated`, `trademark.status_changed`, `trademark.retracted`, `trademark.corrected`. |
| `epoch`      | `all` \| `current`               | `all` (default) returns alerts across every version of the watch's query. Set to `current` to keep only alerts matching the watch's present query.     |

## Reconciliation pattern

For defense-in-depth on top of webhooks, run a periodic job that asks Signa "did you ever fire these IDs?", useful when you suspect a webhook outage.

```ts theme={null}
const recentIds = await myStore.alertsLast48h();
const confirmed = await signa.alerts.lookup(recentIds);
const seen = new Set(confirmed.map((a) => a.id));
for (const id of recentIds) {
  if (!seen.has(id)) reportPossibleLoss(id);
}
```

`alerts.lookup()` takes an array of 1-100 prefixed alert IDs (`alt_*`) per call:

```ts theme={null}
const alerts = await signa.alerts.lookup([
  'alt_8kLm2nPq',
  'alt_9pQs3vNk',
  'alt_7hRt4xQw',
]);
// alerts is an array of Alert objects, unknown or cross-org IDs are
// silently omitted, so alerts.length <= ids.length
```

The endpoint is organization-scoped. Malformed IDs, anything that is not a well-formed `alt_*` ID, fail the whole request with `400 validation_error`, listing each bad entry by index. Well-formed but unknown IDs, and IDs that belong to another organization, are silently dropped from the result. Any gap between the IDs you sent and the alerts you got back is real.

## Severity-based routing

A typical production setup runs two jobs on different cadences:

| Cadence      | Filter              | What it does                                            |
| ------------ | ------------------- | ------------------------------------------------------- |
| Every minute | `severity=critical` | Pages on-call.                                          |
| Hourly       | (no filter)         | Catches `normal` and `high` for the daily review queue. |

```ts theme={null}
const critical = await signa.alerts.list({ severity: 'critical' });
for await (const a of critical) routeToOnCallAttorney(a);
```

## Provenance: every alert's evidence chain

Each alert carries a `provenance` block so you can trace it from the office publication to the moment Signa detected it, and see whether that detection was on time.

```json theme={null}
"provenance": {
  "sync_run": "srun_01HK...",
  "content_version": 7,
  "detection_latency_seconds": 9840,
  "late_detection": false
}
```

| Field                       | Meaning                                                                                                                                                                                                                                                                                                      |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sync_run`                  | The ingestion run that detected this change, as a prefixed `srun_` ID. Cross-references `last_relevant_sync_run.sync_run` on the [watch diagnostics](/api-reference/monitoring/watches/diagnostics) endpoint. **Omitted** (not `null`) when the alert predates provenance tracking and has no derivable run. |
| `content_version`           | The record version the alert froze against. Same value used for alert deduplication.                                                                                                                                                                                                                         |
| `detection_latency_seconds` | `created_at - occurred_at`: the gap between when the office published the change (`timestamps.occurred_at`) and when Signa detected it (`timestamps.created_at`). `null` when the source did not publish a timestamp we could anchor to, or published a timestamp later than our detection time.             |
| `late_detection`            | `true` when `detection_latency_seconds` exceeded Signa's internal service-level target for that office. It is always `false` when the latency is unknown: Signa never flags lateness it cannot prove.                                                                                                        |

`late_detection` is a per-office judgement. Offices that publish daily have a tight target; offices that publish weekly or monthly have a proportionally looser one. A `false` flag means "detected within the window we hold ourselves to for that office", not "instant".

Use `detection_latency_seconds` for your own SLA dashboards, and `late_detection` as a ready-made trigger for exception review when Signa was slower than intended.
