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

# Create and manage alerts

> Resolve an entity, preview how often an alert would fire, create it, and update or pause it later — from the dashboard or the API.

export const APIKeysLink = ({children}) => {
  return <a href="https://platform.particle.pro/tokens">{children}</a>;
};

You can create an alert two ways: point-and-click in the platform dashboard, or programmatically through the API. Both produce the same resource and both are covered below.

## Before you start

* **Plan** — Alerts require a **Team**, **Business**, or **Enterprise** plan. On a plan that doesn't include them, the create call returns [`plan_does_not_support_alerts`](/errors/plan_does_not_support_alerts).
* **API key** — Alerts CRUD works with a standard `X-API-Key`. Create one on the <APIKeysLink>API keys page</APIKeysLink>. Your key is bound to a project, and the create endpoint is project-scoped — pass that same project's ID in the path.
* **A notification email** — Matches are delivered by email to a verified address. Auto-verification works for dashboard users; API-key callers must use an address that's already verified for the organization. See [Notification emails](#notification-emails).

## Create from the dashboard

Creating alerts programmatically? The dashboard flow is for humans — skip to [Create with the API](#create-with-the-api).

## Create with the API

### 1. Resolve the entity

An alert watches entities by canonical ID, not by name. Resolve the entity first, then use its `id` as the `entity_id`. `entity_type` is one of `PERSON`, `COMPANY`, or `KNOWLEDGE_GRAPH_ENTITY`:

| `entity_type`            | Resolve with                                                                | Use as `entity_id`                                                                   |
| ------------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `COMPANY`                | [`GET /v1/companies/{id}`](/companies/overview)                             | the company's `id`                                                                   |
| `KNOWLEDGE_GRAPH_ENTITY` | [`GET /v1/entities/{id}`](/knowledge-graph/entities)                        | the entity's `id` (people, organizations, places, and topics in the knowledge graph) |
| `PERSON`                 | a Particle person record, e.g. a resolved [podcast guest](/podcasts/guests) | the person's `id`                                                                    |

```bash theme={"dark"}
# Resolve a company
curl "https://api.particle.pro/v1/companies/nvidia" \
  -H "X-API-Key: $PARTICLE_API_KEY"
# → "id": "3CensCwu5G2oKCFgPrNf89"

# Resolve a knowledge-graph entity (a person, here)
curl "https://api.particle.pro/v1/entities/sam-altman" \
  -H "X-API-Key: $PARTICLE_API_KEY"
# → "id": "17PzxG1t12xzno"
```

<Warning>
  Use the canonical `id`, not the slug. For `PERSON` and `COMPANY`, `entity_id` must decode to a valid UUID — a slug like `nvidia` is rejected with a `422`. A well-formed `id` that doesn't match a real entity is **accepted**, but the alert is created without the enriched `name`/`image_url` and won't match anything, so confirm the ID resolves first.
</Warning>

### 2. Preview match frequency (optional)

Before you commit, run a preview to see how often the alert would have fired over the past `window_days` days (default 7, max 30). The sweep is asynchronous, and the `POST` returns only status fields (`preview_id`, `status`, `window_days`) — never the counts. A fresh sweep returns `202 Accepted` with `status: "in_progress"`; poll `GET /v1/alerts/preview/{previewId}` until `status` is terminal (`completed` or `failed`), then read `total_matches`, `by_day`, and `sample` from that `GET` response. If an identical preview ran within the past hour, the `POST` returns `200 OK` with `status: "completed"` instead — the result is ready, so a single `GET` retrieves it without a polling loop.

```bash theme={"dark"}
# Kick off the sweep
curl -X POST "https://api.particle.pro/v1/projects/$PARTICLE_PROJECT_ID/alerts/preview?window_days=7" \
  -H "X-API-Key: $PARTICLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "ENTITY_MENTION",
    "entities": [
      { "entity_type": "COMPANY", "entity_id": "3CensCwu5G2oKCFgPrNf89" }
    ]
  }'
```

```json Response (202 Accepted) theme={"dark"}
{
  "preview_id": "9f1c2a7b8e3d4f60",
  "status": "in_progress",
  "window_days": 7
}
```

```bash theme={"dark"}
# Poll until status is completed or failed
curl "https://api.particle.pro/v1/alerts/preview/9f1c2a7b8e3d4f60" \
  -H "X-API-Key: $PARTICLE_API_KEY"
```

```json Response (200 OK) theme={"dark"}
{
  "status": "completed",
  "window_days": 7,
  "started_at": "2026-06-16T15:04:00Z",
  "completed_at": "2026-06-16T15:04:06Z",
  "total_matches": 42,
  "by_day": [
    { "date": "2026-06-10", "count": 5 },
    { "date": "2026-06-11", "count": 9 }
  ],
  "sample": [
    {
      "kind": "ENTITY_MENTION",
      "entity_type": "COMPANY",
      "entity_id": "3CensCwu5G2oKCFgPrNf89",
      "mention_count": 4,
      "episode": { "title": "…", "podcast_title": "…" }
    }
  ]
}
```

`total_matches` is the count over the whole window; `by_day` is the per-day histogram (empty days included so a chart has no gaps); `sample` is the few most-recent matches with full payload so you can show "here's what it would have caught." Preview IDs are deterministic — repeating the same body within the hour returns the same `preview_id` and a cached result, so polling is cheap.

If the sweep errors, the same endpoint returns `status: "failed"` with a human-readable `error` string instead of counts — treat `failed` as terminal alongside `completed` and stop polling. Previews are retained for one hour; an expired `preview_id` returns `404`.

### 3. Create the alert

`POST` the configuration to the project-scoped endpoint. The body sets the title, kind, delivery cadence, the entities to watch, and one or more email channels.

<CodeGroup>
  ```bash curl theme={"dark"}
  curl -X POST "https://api.particle.pro/v1/projects/$PARTICLE_PROJECT_ID/alerts" \
    -H "X-API-Key: $PARTICLE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Nvidia mentions on podcasts",
      "kind": "ENTITY_MENTION",
      "delivery_cadence": "REALTIME",
      "entities": [
        { "entity_type": "COMPANY", "entity_id": "3CensCwu5G2oKCFgPrNf89" }
      ],
      "notifications": [
        { "type": "EMAIL", "email": "you@example.com" }
      ]
    }'
  ```

  ```js JavaScript theme={"dark"}
  const res = await fetch(
    `https://api.particle.pro/v1/projects/${process.env.PARTICLE_PROJECT_ID}/alerts`,
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.PARTICLE_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: "Nvidia mentions on podcasts",
        kind: "ENTITY_MENTION",
        delivery_cadence: "REALTIME",
        entities: [
          { entity_type: "COMPANY", entity_id: "3CensCwu5G2oKCFgPrNf89" },
        ],
        notifications: [{ type: "EMAIL", email: "you@example.com" }],
      }),
    },
  );
  const alert = await res.json();
  ```
</CodeGroup>

```json Response (200) theme={"dark"}
{
  "id": "8Qw2mRk7Tf9LpZ",
  "project_id": "Vb3xN1aPq8",
  "title": "Nvidia mentions on podcasts",
  "kind": "ENTITY_MENTION",
  "delivery_cadence": "REALTIME",
  "is_active": true,
  "entities": [
    {
      "entity_type": "COMPANY",
      "entity_id": "3CensCwu5G2oKCFgPrNf89",
      "name": "Nvidia",
      "image_url": "https://cdn.particle.pro/…"
    }
  ],
  "notifications": [
    { "id": "Nk4d…", "type": "EMAIL", "email": "you@example.com" }
  ],
  "created_at": "2026-06-16T15:10:00Z",
  "updated_at": "2026-06-16T15:10:00Z"
}
```

The `created_by` field is omitted here because the request authenticated with an API key; it's populated with the creating user only for alerts created by a signed-in dashboard user.

To alert on a person's **appearances** rather than mentions, set `kind` to `PODCAST_SPEAKER` and watch a person entity — either a `PERSON` record or the person's knowledge-graph entity. This fires only when the person is an identified speaker on the episode — a guest, panelist, correspondent, or audience member — not when they're merely talked about.

```bash theme={"dark"}
curl -X POST "https://api.particle.pro/v1/projects/$PARTICLE_PROJECT_ID/alerts" \
  -H "X-API-Key: $PARTICLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Sam Altman podcast appearances",
    "kind": "PODCAST_SPEAKER",
    "delivery_cadence": "REALTIME",
    "entities": [
      { "entity_type": "KNOWLEDGE_GRAPH_ENTITY", "entity_id": "17PzxG1t12xzno" }
    ],
    "notifications": [
      { "type": "EMAIL", "email": "you@example.com" }
    ]
  }'
```

A few rules worth knowing:

* **`kind` is fixed at creation.** Switching mention vs appearance later would invalidate the match history, so create a new alert instead.
* **`delivery_cadence` defaults to `REALTIME`.** Use `DAILY` or `WEEKLY` for bundled digest emails — see [Delivery frequency](/alerts/overview#delivery-frequency).
* **`is_active` defaults to `true`.** Pass `false` to create the alert paused: it won't match new episodes or send email going forward, and it doesn't count against your plan's allowance until enabled. The one-time creation backfill still runs, so a paused alert can still show up to 7 days of historical (backfilled) matches.
* **A new alert is backfilled.** Right after creation — even if you create it paused — Particle sweeps the past 7 days so the alert isn't empty. Backfilled matches never send email — see [Alert results → Backfill](/alerts/results#backfill).
* **`filters` is optional.** Omit to surface every detected match. Set any of `languages`, `relevance`, `source_popularity`, or `speaker_roles` to narrow what's delivered — the same filter shape applies on `PATCH /v1/alerts/{id}`. See [Filtering matches](/alerts/overview#filtering-matches) for the full spec.

### Narrow an alert with filters

A second example shows the four filter axes in action — an English-language `PODCAST_SPEAKER` alert that fires only when Sam Altman appears as a guest on a top-5% show:

```bash theme={"dark"}
curl -X POST "https://api.particle.pro/v1/projects/$PARTICLE_PROJECT_ID/alerts" \
  -H "X-API-Key: $PARTICLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Sam Altman — guest spots on top podcasts",
    "kind": "PODCAST_SPEAKER",
    "delivery_cadence": "REALTIME",
    "entities": [
      { "entity_type": "KNOWLEDGE_GRAPH_ENTITY", "entity_id": "17PzxG1t12xzno" }
    ],
    "filters": {
      "languages": ["en"],
      "relevance": "RELEVANT",
      "source_popularity": "POPULAR",
      "speaker_roles": ["GUEST"]
    },
    "notifications": [
      { "type": "EMAIL", "email": "you@example.com" }
    ]
  }'
```

Each axis defaults to "no filter", so you only need the ones that narrow the way you want — sending `{ "source_popularity": "POPULAR" }` alone is a complete filter set. On `PATCH /v1/alerts/{id}`, omitting `filters` leaves the existing filter set unchanged; pass `"filters": {}` to clear all four axes back to their defaults.

<Tip>
  Email isn't the only destination. Add a `SLACK` channel to post matches to a Slack channel (`{ "type": "SLACK", "slack_connection_id": "…", "slack_channel_id": "…" }`), or a `WEBHOOK` channel to POST them to your own service (`{ "type": "WEBHOOK", "webhook_connection_id": "…" }`). See [Slack](/alerts/slack) and [Webhooks](/alerts/webhooks) for setup.
</Tip>

## Notification emails

Every email channel must be verified for your organization before it receives mail. What gets verified automatically depends on how you authenticate:

* **Dashboard / logged-in users** — your own address and any address belonging to a **member of your organization** are verified automatically the first time you use them.
* **API-key callers** — an API key carries no user identity, so nothing is auto-verified. The address must already be verified for the organization (verify it once from the dashboard, or through the verification-link flow); otherwise the create or update call returns [`email_verification_required`](/errors/email_verification_required).
* **Any third-party address** must confirm a one-time verification link before it receives mail.

Deleting a verified notification email that is still referenced by an alert returns [`notification_email_in_use`](/errors/notification_email_in_use) — remove it from the alert's `notifications` first.

## Update, pause, and delete

`PATCH /v1/alerts/{id}` applies a partial update. Only the fields you send change — but `entities` and `notifications`, when present, **replace the whole set** (they aren't merged).

```bash theme={"dark"}
# Pause an alert without losing its history
curl -X PATCH "https://api.particle.pro/v1/alerts/8Qw2mRk7Tf9LpZ" \
  -H "X-API-Key: $PARTICLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'
```

```bash theme={"dark"}
# Resume it
curl -X PATCH "https://api.particle.pro/v1/alerts/8Qw2mRk7Tf9LpZ" \
  -H "X-API-Key: $PARTICLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": true }'
```

```bash theme={"dark"}
# Delete it (soft delete — existing matches and deliveries are retained for audit)
curl -X DELETE "https://api.particle.pro/v1/alerts/8Qw2mRk7Tf9LpZ" \
  -H "X-API-Key: $PARTICLE_API_KEY"
```

Recipients can also turn an alert off without an account: every alert email has a **Disable this alert** link in its footer that flips the alert to inactive in one click. See [Alert results → Stop delivery](/alerts/results#stop-delivery).

## Errors

| Error                                                                  | When                                                                                                                                            |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| [`plan_does_not_support_alerts`](/errors/plan_does_not_support_alerts) | The organization's plan doesn't include alerts.                                                                                                 |
| [`add_on_required`](/errors/add_on_required)                           | Plan's included alert allowance is reached. Buy an Alerts Pack.                                                                                 |
| [`email_verification_required`](/errors/email_verification_required)   | A third-party notification email hasn't confirmed its link.                                                                                     |
| [`validation_error`](/errors/validation_error)                         | Malformed `entity_id` (e.g. a slug where a canonical `PERSON`/`COMPANY` ID is required), unknown `kind`, or an `EMAIL` channel with no address. |

## Related

* [Alerts overview](/alerts/overview) — concepts, alert kinds, and the plan requirement
* [Alert results](/alerts/results) — read the matches and deliveries an alert produces
* [Slack](/alerts/slack) — post matches to a Slack channel
* [Webhooks](/alerts/webhooks) — deliver matches to your own endpoint as a signed JSON POST
* [Knowledge graph → entities](/knowledge-graph/entities) — resolve people, companies, and topics to watch
