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

# Notifications

> Configure in-app, email, and Slack notifications for events across your buyer workspace.

## Overview

Notifications surface async events from across your buyer workspace — campaign health, creative approvals, agent registrations, optimization suggestions, and more — through three channels: **in-app** (per-user feed), **email** (account-scoped), and **Slack** (account-scoped webhook). In-app opt-ins are tuned per user; email and Slack are admin-managed at the account level.

The notification system gives every operator a structured event feed for things they
care about — campaign health, creative approvals, agent registrations, optimization
suggestions, and more. Each event is delivered to one or more channels (`in_app`,
`email`, Slack), and every user opts into the events they want to see.

## Open the notification inbox in Murph

Buyer and storefront agents can open the **Notifications** Page in Murph. It
lists a bounded page of events, lets you load older results, and keeps **Read**
and **Acknowledged** as separate states. Both states apply only to the signed-in
recipient: reading or acknowledging a broadcast event does not change another
person's inbox.

The Page also shows the current primary-Slack settings for Product Updates and
sustained source-health alerts. Changing delivery does not mark an inbox event
read or acknowledged, and a successful Slack, email, or webhook attempt does
not count as either action. Release history stays in the separate **Release
notes** Page.

For agent builders, v3 `open_page({"page":"notifications"})` returns a
persona-specific Page contract. Buyer and storefront operation aliases are
explicit; if the launch contract is missing, the Page waits instead of
silently trying another API surface.

<Note>
  All endpoints below are mounted under `https://api.interchange.io/api/v2/` and are
  accessible to both buyer and storefront users (role-agnostic shared router).
  The notification feed is filtered server-side by your `in_app` opt-ins plus
  always-on operational event types. A user with no opt-ins can therefore still
  see required storefront health and approval events.
</Note>

## Event taxonomy

Notification types follow a `resource.action` taxonomy so agents can build workflows
around stable event names. The full enum lives in
`apps/api/src/types/notifications.ts`. A representative slice:

<CardGroup cols={2}>
  <Card title="Campaign lifecycle" icon="bullhorn">
    `campaign.created`, `campaign.updated`, `campaign.healthy`,
    `campaign.unhealthy`, `campaign.completed`, `campaign.deleted`
  </Card>

  <Card title="Creative review" icon="image">
    `creative.approved`, `creative.rejected`, `creative.changes_requested`,
    `creative.sync_started`, `creative.sync_completed`, `creative.sync_failed`
  </Card>

  <Card title="Agent registry" icon="robot">
    `salesagent.registered`, `salesagent.available`, `signalsagent.registered`,
    `outcomesagent.registered`, `salesagent.updated`
  </Card>

  <Card title="Optimization" icon="sparkles">
    `optimization.suggestion_received`, `optimization.suggestion_approved`,
    `optimization.suggestion_applied`, `optimization.suggestion_failed`
  </Card>

  <Card title="Measurement & learning" icon="flask">
    `measurement.received`, `measurement.stale`, `learning_cycle.completed`,
    `hypothesis.proven`, `hypothesis.disproven`
  </Card>

  <Card title="System & syndication" icon="bell">
    `system.warning`, `system.error`, `syndication.completed`,
    `syndication.failed`, `audience.synced`
  </Card>

  <Card title="Storefront health & approvals (always-on)" icon="heart-pulse">
    `inventory_source.unhealthy`, `source.action_required`,
    `source.action_resolved`, `salesagent.sync_action_required`,
    `storefront_adapter_credential.action_required`,
    `storefront.composition_paused`, `storefront.products_not_traffickable`,
    `storefront.liveness_degraded`, `storefront.liveness_recovered`,
    `media_buy.approval_requested`, `creative.review_requested`,
    `ad_server.approval_requested`, `inventory_shortfall.review_requested` —
    operational alerts that bypass opt-in for the in-app feed. Storefront
    health/readiness, ad-server approval, and inventory-shortfall alerts use
    account notification settings. Media-buy and creative-review queue alerts
    use the primary or optional escalation audience configured for that approval class;
    email is sent only when the routing stage enables email. Slack is the one
    exception: even always-on types only post
    to Slack once you've added that event type to your Slack configuration's
    enabled events — connecting Slack doesn't turn these on by itself.
  </Card>
</CardGroup>

<Note>
  **Optimization suggestions notify you only when there's a decision to make.**
  You get `optimization.suggestion_received` when a suggestion proposes a budget
  or bid change on a campaign and media buy that are both still active. A
  suggestion that carries no budget or bid change doesn't notify — most often a
  media buy with no optimization goal, where the suggestion is pacing information
  only, but also any run where the optimizer settles on your current allocation.
  Neither does a suggestion whose campaign or media buy is no longer active by the
  time it reaches you. In both cases the suggestion is still recorded against the
  media buy and closed out, so nothing sits in your queue waiting on a decision
  you can't make.
</Note>

<Note>
  **Your storefront's liveness verdict flipping is a proactive notification.**
  When the `liveness.state` on [Get readiness](/v2/storefront/tasks/get-readiness)
  flips away from `live` (to `live_attention` or `live_critical`), or a further
  worsening from `live_attention` to `live_critical`, you get a
  `storefront.liveness_degraded` notification carrying the verdict's own reason
  line and fix path — you don't have to notice the dot change color. Recovery
  back to `live` sends `storefront.liveness_recovered`. A storefront still in
  setup (`state: "setup"`) never alerts — setup is not failure. Repeated sweeps
  of the same unchanged state never re-notify.
</Note>

Each notification carries a `status` (`success`, `error`, `warning`, `info`) and a
`data` object with resource IDs (`campaignId`, `creativeId`, `salesAgentId`, …) plus a
human-readable `message`.

<Note>
  **Pending storefront review now sends a notification.** When a buyer submits a
  creative or media buy that's waiting on a storefront operator's decision, it
  still appears in the operator's Approvals queue and via Murph — and it also
  notifies the configured approval audience. In-app is always on; email and Slack
  follow that approval class's routing rule. See [Reviewing buyer transactions](/v2/storefront/approvals/overview)
  for how operators review and act on pending items.
</Note>

## Synchronous creative sync errors

Creative sync failures are not always reported as notifications. When a media-buy create or update targets a library-only seller and `sync_creatives` fails before the media-buy request is sent, the API returns a synchronous error instead of continuing silently.

Handle this as an API failure and retry after correcting the creative or seller issue. The message includes the seller and cause:

```text theme={null}
Creative sync to sales agent {id} failed before media buy update: {reason}
```

## Channels

| Channel  | Configuration                                                       | Best for                     |
| -------- | ------------------------------------------------------------------- | ---------------------------- |
| `in_app` | Per-user opt-ins via `PUT /notification-preferences`                | Live operator UI feed        |
| `email`  | Account-level `notificationEmail` plus event-specific user delivery | Critical alerts, audit trail |
| Slack    | Account-level webhook + per-event-type filter                       | Team channels, war rooms     |

<Tip>
  `email` and Slack opt-ins are **account-scoped** and managed by admins. `in_app`
  opt-ins are **per-user** so individuals can tune their personal feed. Approval
  work for media buys and creative review is different: the approval routing rule
  names its primary users and any optional escalation users, and its channel settings decide whether
  those users also receive email or the customer Slack channel receives one post.
  A missing account notification email does not broadcast those routed approval
  classes to every active user. Ad-server and inventory-shortfall approvals still
  use account notification settings until dedicated routes are available.
</Tip>

## Per-user in-app preferences

Operators control which events appear in their personal feed.

<Steps>
  <Step title="List the user's current opt-ins">
    ```bash theme={null}
    curl https://api.interchange.io/api/v2/notification-preferences \
      -H "Authorization: Bearer $SCOPE3_API_KEY"
    ```

    Returns `{ "optIns": [{ "notificationType": "campaign.unhealthy", "channel": "in_app" }, ...] }`.
  </Step>

  <Step title="Replace opt-ins (full set)">
    `PUT /notification-preferences` replaces the user's entire opt-in list. Send up
    to 200 entries.

    ```bash theme={null}
    curl -X PUT https://api.interchange.io/api/v2/notification-preferences \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "optIns": [
          { "notificationType": "campaign.unhealthy", "channel": "in_app" },
          { "notificationType": "creative.rejected", "channel": "in_app" },
          { "notificationType": "salesagent.registered", "channel": "in_app" }
        ]
      }'
    ```
  </Step>
</Steps>

## Account-level email opt-ins (admin)

Admins set the recipient address and which event types should generate email.

<Warning>
  Changing `notificationEmail` is **sensitive** — the recipient receives every
  email-routed event for the account (including alert details that may contain
  campaign IDs and account context). Treat the field like any other admin
  credential: restrict who can edit it, and audit changes. Mutations to
  `notificationEmail` are recorded on the audit log and visible in the buyer
  activity feed.
</Warning>

<CodeGroup>
  ```bash Set the notification email (admin) theme={null}
  curl -X PUT https://api.interchange.io/api/v2/notification-email \
    -H "Authorization: Bearer $SCOPE3_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "notificationEmail": "alerts@brand.example.com" }'
  ```

  ```bash Get the notification email theme={null}
  curl https://api.interchange.io/api/v2/notification-email \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```

  ```bash Replace account-level opt-ins (admin) theme={null}
  curl -X PUT https://api.interchange.io/api/v2/notification-email/opt-ins \
    -H "Authorization: Bearer $SCOPE3_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "optIns": [
        { "notificationType": "campaign.unhealthy", "channel": "email" },
        { "notificationType": "creative.rejected",  "channel": "email" }
      ]
    }'
  ```

  ```bash List account-level opt-ins theme={null}
  curl https://api.interchange.io/api/v2/notification-email/opt-ins \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```
</CodeGroup>

<Note>
  `PUT /notification-email` and `PUT /notification-email/opt-ins` require the caller to
  have the `ADMIN` or `SUPER_ADMIN` role. Pass `notificationEmail: null` to clear the
  account-wide recipient; per-user email delivery still follows the rules above.
</Note>

## Slack configuration (admin)

Slack delivery uses an [Incoming Webhook](https://api.slack.com/messaging/webhooks)
URL. Webhooks are admin-only and stored at the account level.

### Murph channel delivery settings

Product Updates and source outage alerts use your account's primary
Murph Slack channel rather than the incoming-webhook event filter above. An
active account member can ask Murph to show or change these settings; Murph
confirms the write before applying it. You can also use the shared v2 API:

```bash List primary-channel delivery settings theme={null}
curl https://api.interchange.io/api/v2/notification-delivery-preferences \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```bash Turn off Product Updates in the primary channel theme={null}
curl -X PUT https://api.interchange.io/api/v2/notification-delivery-preferences \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "stream": "product_updates", "enabled": false }'
```

```bash Send a Slack alert after a source outage lasts 30 minutes theme={null}
curl -X PUT https://api.interchange.io/api/v2/notification-delivery-preferences \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "stream": "source_health_alerts",
    "enabled": true,
    "thresholdMinutes": 30
  }'
```

Source outage Slack alerts can be sent after 10, 30, or 60 minutes, or turned
off. Each alert also includes controls for future outages. Turning these Slack
alerts off does **not** disable immediate in-app or email health notifications.

Product Updates currently support on/off only. Their delivery schedule is fixed;
the API does not expose a frequency value that the sender cannot honor.

<Note>
  **Email opt-ins and Slack `enabledEventTypes` are independent filters.** The account-level email opt-in list (`/notification-email/opt-ins`) controls which events generate **email**; the Slack `enabledEventTypes` list on `/slack-configuration` is a separate per-channel filter applied to **Slack delivery only**. To get a Slack alert for a given event type, that type must be present in `enabledEventTypes` — the email opt-in list has no effect on Slack. Configure both lists independently to route the same event to the channels you want.
</Note>

<Steps>
  <Step title="Create or update the webhook">
    ```bash theme={null}
    curl -X PUT https://api.interchange.io/api/v2/slack-configuration \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "webhookUrl": "https://hooks.slack.com/services/T_REPLACE/B_REPLACE/REPLACE_TOKEN",
        "enabled": true,
        "enabledEventTypes": [
          "campaign.unhealthy",
          "creative.rejected",
          "system.error"
        ]
      }'
    ```

    The URL must start with `https://hooks.slack.com/services/`. Provide at least one
    event type — only events on this list are forwarded to Slack.

    <Warning>
      A Slack incoming-webhook URL is a **credential**: anyone who has the URL
      can post messages to that channel without authenticating. Never paste a
      real webhook URL into source files, tickets, screenshots, or shared
      docs. Store the URL in a secret manager and rotate it immediately if it
      leaks (revoke + reissue from the Slack app configuration).
    </Warning>
  </Step>

  <Step title="Send a test message">
    ```bash theme={null}
    curl -X POST https://api.interchange.io/api/v2/slack-configuration/test \
      -H "Authorization: Bearer $SCOPE3_API_KEY"
    ```

    Posts a confirmation message to the configured webhook so the team can verify
    delivery before relying on it.
  </Step>

  <Step title="Read the current configuration">
    ```bash theme={null}
    curl https://api.interchange.io/api/v2/slack-configuration \
      -H "Authorization: Bearer $SCOPE3_API_KEY"
    ```

    The response masks the webhook URL by default.
  </Step>

  <Step title="Disable Slack delivery">
    ```bash theme={null}
    curl -X DELETE https://api.interchange.io/api/v2/slack-configuration \
      -H "Authorization: Bearer $SCOPE3_API_KEY"
    ```
  </Step>
</Steps>

## Reading the in-app feed

```bash theme={null}
curl "https://api.interchange.io/api/v2/notifications?unreadOnly=true&limit=25" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

| Query param    | Type             | Notes                                             |
| -------------- | ---------------- | ------------------------------------------------- |
| `brandAgentId` | number           | Limit to a single brand agent                     |
| `campaignId`   | string           | Limit to a single campaign                        |
| `creativeId`   | string           | Limit to a single creative                        |
| `status`       | enum             | `success` / `error` / `warning` / `info`          |
| `types`        | comma list       | One or more `NotificationEventType` values        |
| `unreadOnly`   | `true` / `false` | Hide already-read items                           |
| `limit`        | 1..100           | Default 50                                        |
| `offset`       | int              | Number of items to skip (offset-based pagination) |

The response includes the page of notifications, plus `totalCount`, `unreadCount`, and
`hasMore` for pagination.

<Tip>
  The list endpoint already filters to event types the calling user opted into on the
  `in_app` channel — there is no need to pass `types` from the UI. If the user has not
  opted into any in-app types the response is an empty array (with `totalCount: 0`).
</Tip>

## Mark read / acknowledge

A notification has two completion states:

* **read** — surfaced to the user; safe to demote in the UI.
* **acknowledged** — the user has acted on it (e.g. approved a suggestion). The feed
  hides acknowledged items by default.

<CodeGroup>
  ```bash Mark a single item read theme={null}
  curl -X POST https://api.interchange.io/api/v2/notifications/$ID/read \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```

  ```bash Mark every item read (optionally scoped to a brand agent) theme={null}
  curl -X POST https://api.interchange.io/api/v2/notifications/read-all \
    -H "Authorization: Bearer $SCOPE3_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "brandAgentId": 12345 }'
  ```

  ```bash Acknowledge an item theme={null}
  curl -X POST https://api.interchange.io/api/v2/notifications/$ID/acknowledge \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```
</CodeGroup>

## Endpoint reference

| Method   | Path                                 | Purpose                                                      |
| -------- | ------------------------------------ | ------------------------------------------------------------ |
| `GET`    | `/notifications`                     | List in-app notifications (filtered by user opt-ins)         |
| `POST`   | `/notifications/read-all`            | Mark all visible notifications read                          |
| `POST`   | `/notifications/:id/read`            | Mark one notification read                                   |
| `POST`   | `/notifications/:id/acknowledge`     | Mark one notification acknowledged                           |
| `GET`    | `/storefront/approval-routing`       | List approval audiences, channels, and SLA settings          |
| `PUT`    | `/storefront/approval-routing/:kind` | Replace routing for `MEDIA_BUY` or `CREATIVE_REVIEW` (admin) |
| `GET`    | `/notification-preferences`          | List the calling user's in-app opt-ins                       |
| `PUT`    | `/notification-preferences`          | Replace the user's in-app opt-ins                            |
| `GET`    | `/notification-email`                | Read the account's email recipient                           |
| `PUT`    | `/notification-email`                | Set the email recipient (admin)                              |
| `GET`    | `/notification-email/opt-ins`        | List account-level email/Slack opt-ins                       |
| `PUT`    | `/notification-email/opt-ins`        | Replace account-level opt-ins (admin)                        |
| `GET`    | `/slack-configuration`               | Read Slack webhook config (admin, URL masked)                |
| `PUT`    | `/slack-configuration`               | Create or update Slack webhook (admin)                       |
| `POST`   | `/slack-configuration/test`          | Send a test message to the webhook (admin)                   |
| `DELETE` | `/slack-configuration`               | Remove Slack webhook (admin)                                 |

<Note>
  All endpoints validate the standard buyer auth context (`customerId`, `userId`,
  `userRole`). Admin-only endpoints reject non-admin callers with `ACCESS_DENIED`.
</Note>
