# Notifications

Source: https://opvs.ai/docs/api/notifications

## `POST` /api/v1/notifications/email

**Send Email Notification**

Send an email notification to all admins of a brand.

**Request body**

```bash
curl -X POST 'https://app.opvs.ai/api/v1/notifications/email' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "brand_id": 0,
  "subject": "string",
  "heading": "string",
  "body_html": "string",
  "body_text": "string",
  "category": "heartbeat"
}'
```

```python
import httpx

resp = httpx.post(
    "https://app.opvs.ai/api/v1/notifications/email",
    headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
    json={"brand_id": 0, "subject": "string", "heading": "string", "body_html": "string", "body_text": "string", "category": "heartbeat"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/email", {
  method: "POST",
  headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
  body: JSON.stringify({"brand_id": 0, "subject": "string", "heading": "string", "body_html": "string", "body_text": "string", "category": "heartbeat"})
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{"brand_id": 0, "subject": "string", "heading": "string", "body_html": "string", "body_text": "string", "category": "heartbeat"}`)
	req, _ := http.NewRequest("POST", "https://app.opvs.ai/api/v1/notifications/email", body)
	req.Header.Set("Authorization", "Bearer <token>")
	req.Header.Set("Content-Type", "application/json")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `POST` /api/v1/notifications/in-app

**Send In App Notification**

Create an in-app notification for the deliverable members of a brand.

**Request body**

```bash
curl -X POST 'https://app.opvs.ai/api/v1/notifications/in-app' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "brand_id": 0,
  "title": "string",
  "body": "string",
  "severity": "info",
  "link": "string",
  "category": "heartbeat"
}'
```

```python
import httpx

resp = httpx.post(
    "https://app.opvs.ai/api/v1/notifications/in-app",
    headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
    json={"brand_id": 0, "title": "string", "body": "string", "severity": "info", "link": "string", "category": "heartbeat"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/in-app", {
  method: "POST",
  headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
  body: JSON.stringify({"brand_id": 0, "title": "string", "body": "string", "severity": "info", "link": "string", "category": "heartbeat"})
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{"brand_id": 0, "title": "string", "body": "string", "severity": "info", "link": "string", "category": "heartbeat"}`)
	req, _ := http.NewRequest("POST", "https://app.opvs.ai/api/v1/notifications/in-app", body)
	req.Header.Set("Authorization", "Bearer <token>")
	req.Header.Set("Content-Type", "application/json")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications/preferences

**Get Preferences**

Every catalog event with this user's effective (email, in_app, delivery) for the active brand. ?format=yaml|md for agents.

**Parameters**

- `format` (query, any, optional)

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications/preferences' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications/preferences",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/preferences", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications/preferences", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications/preferences/dnd

**Get Dnd**

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications/preferences/dnd' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications/preferences/dnd",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/preferences/dnd", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications/preferences/dnd", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response

## `PATCH` /api/v1/notifications/preferences/dnd

**Update Dnd**

**Request body**

```bash
curl -X PATCH 'https://app.opvs.ai/api/v1/notifications/preferences/dnd' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "timezone": "string",
  "dnd_start": "string",
  "dnd_end": "string",
  "dnd_days_off": 0,
  "clear_window": false
}'
```

```python
import httpx

resp = httpx.patch(
    "https://app.opvs.ai/api/v1/notifications/preferences/dnd",
    headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
    json={"timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/preferences/dnd", {
  method: "PATCH",
  headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
  body: JSON.stringify({"timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false})
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{"timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false}`)
	req, _ := http.NewRequest("PATCH", "https://app.opvs.ai/api/v1/notifications/preferences/dnd", body)
	req.Header.Set("Authorization", "Bearer <token>")
	req.Header.Set("Content-Type", "application/json")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `PATCH` /api/v1/notifications/preferences/{event_key}

**Update Preference**

Toggle one event's channels/delivery for the active brand.

**Parameters**

- `event_key` (path, string, required)

**Request body**

```bash
curl -X PATCH 'https://app.opvs.ai/api/v1/notifications/preferences/{event_key}' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "email": true,
  "in_app": true,
  "delivery": "string"
}'
```

```python
import httpx

resp = httpx.patch(
    "https://app.opvs.ai/api/v1/notifications/preferences/{event_key}",
    headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
    json={"email": true, "in_app": true, "delivery": "string"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/preferences/{event_key}", {
  method: "PATCH",
  headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
  body: JSON.stringify({"email": true, "in_app": true, "delivery": "string"})
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{"email": true, "in_app": true, "delivery": "string"}`)
	req, _ := http.NewRequest("PATCH", "https://app.opvs.ai/api/v1/notifications/preferences/{event_key}", body)
	req.Header.Set("Authorization", "Bearer <token>")
	req.Header.Set("Content-Type", "application/json")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications/unsubscribe

**Unsubscribe Landing**

Body-link landing — applies the unsubscribe and shows a confirmation.

**Parameters**

- `token` (query, string, required)

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications/unsubscribe' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications/unsubscribe",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/unsubscribe", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications/unsubscribe", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `POST` /api/v1/notifications/unsubscribe

**Unsubscribe One Click**

RFC 8058 one-click target (the MUA POSTs here). No body required.

**Parameters**

- `token` (query, string, required)

```bash
curl -X POST 'https://app.opvs.ai/api/v1/notifications/unsubscribe' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.post(
    "https://app.opvs.ai/api/v1/notifications/unsubscribe",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/unsubscribe", {
  method: "POST",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("POST", "https://app.opvs.ai/api/v1/notifications/unsubscribe", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications/feed

**Get Notification Feed**

Keyset-paginated in-app bell feed, newest first. Pass the previous page's ``next_cursor`` back as ``cursor_created_at`` + ``cursor_id``.

**Parameters**

- `limit` (query, integer, optional)
- `cursor_created_at` (query, any, optional)
- `cursor_id` (query, any, optional)
- `unread_only` (query, boolean, optional)

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications/feed' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications/feed",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/feed", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications/feed", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications

**List Notifications**

Alias of ``GET /notifications/feed`` at the router root — a parity convenience (no existing consumer requires it; see DISCOVERY §2 E1). Returns the same keyset-paginated in-app bell feed for the (user, active brand).

**Parameters**

- `limit` (query, integer, optional)
- `cursor_created_at` (query, any, optional)
- `cursor_id` (query, any, optional)
- `unread_only` (query, boolean, optional)

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications/unread-count

**Get Unread Count**

Count of the user's unread in-app notifications for the active brand.

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications/unread-count' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications/unread-count",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/unread-count", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications/unread-count", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response

## `POST` /api/v1/notifications/read-all

**Mark All Notifications Read**

Mark every unread in-app notification for (user, brand) read.

```bash
curl -X POST 'https://app.opvs.ai/api/v1/notifications/read-all' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.post(
    "https://app.opvs.ai/api/v1/notifications/read-all",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/read-all", {
  method: "POST",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("POST", "https://app.opvs.ai/api/v1/notifications/read-all", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response

## `POST` /api/v1/notifications/{notification_id}/read

**Mark Notification Read**

Mark one notification read. The body carries ``created_at`` (the row's partition key) so the UPDATE prunes to a single partition (LANDMINE 5). Returns ``updated=false`` if the row isn't the caller's or was already read.

**Parameters**

- `notification_id` (path, integer, required)

**Request body**

```bash
curl -X POST 'https://app.opvs.ai/api/v1/notifications/{notification_id}/read' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "created_at": "2026-01-01T00:00:00Z"
}'
```

```python
import httpx

resp = httpx.post(
    "https://app.opvs.ai/api/v1/notifications/{notification_id}/read",
    headers={"Authorization": "Bearer <token>", "Content-Type": "application/json"},
    json={"created_at": "2026-01-01T00:00:00Z"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/{notification_id}/read", {
  method: "POST",
  headers: { "Authorization": "Bearer <token>", "Content-Type": "application/json" },
  body: JSON.stringify({"created_at": "2026-01-01T00:00:00Z"})
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{"created_at": "2026-01-01T00:00:00Z"}`)
	req, _ := http.NewRequest("POST", "https://app.opvs.ai/api/v1/notifications/{notification_id}/read", body)
	req.Header.Set("Authorization", "Bearer <token>")
	req.Header.Set("Content-Type", "application/json")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response
- `422` — Validation Error

## `GET` /api/v1/notifications/stream

**Notification Stream**

Server-Sent Events stream of ``notification.created`` frames for the authenticated user. The browser's EventSource sends the session cookie, so auth resolves via ``get_current_brand``. Frames are keyed PER USER and carry ``brand_id`` — the client filters to the active brand (LANDMINE 4).

```bash
curl -X GET 'https://app.opvs.ai/api/v1/notifications/stream' \
  -H 'Authorization: Bearer <token>'
```

```python
import httpx

resp = httpx.get(
    "https://app.opvs.ai/api/v1/notifications/stream",
    headers={"Authorization": "Bearer <token>"},
)
resp.raise_for_status()
print(resp.json())
```

```javascript
const resp = await fetch("https://app.opvs.ai/api/v1/notifications/stream", {
  method: "GET",
  headers: { "Authorization": "Bearer <token>" }
});
const data = await resp.json();
console.log(data);
```

```go
package main

import (
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://app.opvs.ai/api/v1/notifications/stream", nil)
	req.Header.Set("Authorization", "Bearer <token>")
	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
}
```

**Responses**

- `200` — Successful Response

## Send Email Notification

`POST https://app.opvs.ai/api/v1/notifications/email`

Send an email notification to all admins of a brand.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Send In App Notification

`POST https://app.opvs.ai/api/v1/notifications/in-app`

Create an in-app notification for the deliverable members of a brand.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Get Preferences

`GET https://app.opvs.ai/api/v1/notifications/preferences`

Every catalog event with this user's effective (email, in_app, delivery)
for the active brand. ?format=yaml|md for agents.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Get Dnd

`GET https://app.opvs.ai/api/v1/notifications/preferences/dnd`

## Update Dnd

`PATCH https://app.opvs.ai/api/v1/notifications/preferences/dnd`

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Update Preference

`PATCH https://app.opvs.ai/api/v1/notifications/preferences/{event_key}`

Toggle one event's channels/delivery for the active brand.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Unsubscribe Landing

`GET https://app.opvs.ai/api/v1/notifications/unsubscribe`

Body-link landing — applies the unsubscribe and shows a confirmation.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Unsubscribe One Click

`POST https://app.opvs.ai/api/v1/notifications/unsubscribe`

RFC 8058 one-click target (the MUA POSTs here). No body required.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Get Notification Feed

`GET https://app.opvs.ai/api/v1/notifications/feed`

Keyset-paginated in-app bell feed, newest first. Pass the previous page's
``next_cursor`` back as ``cursor_created_at`` + ``cursor_id``.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## List Notifications

`GET https://app.opvs.ai/api/v1/notifications`

Alias of ``GET /notifications/feed`` at the router root — a parity
convenience (no existing consumer requires it; see DISCOVERY §2 E1). Returns
the same keyset-paginated in-app bell feed for the (user, active brand).

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Get Unread Count

`GET https://app.opvs.ai/api/v1/notifications/unread-count`

Count of the user's unread in-app notifications for the active brand.

## Mark All Notifications Read

`POST https://app.opvs.ai/api/v1/notifications/read-all`

Mark every unread in-app notification for (user, brand) read.

## Mark Notification Read

`POST https://app.opvs.ai/api/v1/notifications/{notification_id}/read`

Mark one notification read. The body carries ``created_at`` (the row's
partition key) so the UPDATE prunes to a single partition (LANDMINE 5).
Returns ``updated=false`` if the row isn't the caller's or was already
read.

| Code | When | What to do |
| --- | --- | --- |
| 422 | Validation Error |  |

## Notification Stream

`GET https://app.opvs.ai/api/v1/notifications/stream`

Server-Sent Events stream of ``notification.created`` frames for the
authenticated user. The browser's EventSource sends the session cookie, so
auth resolves via ``get_current_brand``. Frames are keyed PER USER and carry
``brand_id`` — the client filters to the active brand (LANDMINE 4).
